-
Notifications
You must be signed in to change notification settings - Fork 58
Introduction to Patching
Version 3.0.0 of HugsLib (for A17) brings significant changes to the way we modify code in the base game. All detour-related functionality has been removed in favor of the Harmony library, which from now on will come included with HugsLib.
This is great news, because it offers exciting new possibilities for modding and will significantly reduce the amount of time and effort required to make mods, if used correctly. It will likely take some time to get used to the new system, though. This page should help ease the transition from detouring to patching.
-
Non-destructive patching
With detours, an entire method had to be replaced, even if it was just to hook into its execution flow. Now hooks can be added to the start and end of the method, and even parts of the method itself can be modified without damaging the rest. -
Improved compatibility
Two mods that detoured the same method would be incompatible with each other. With patching, a multitude of patches can exist on the same method without interfering with each other. Each can modify method arguments and return values without needing to know about other patches on the same method. -
Method body modification
A optional patching feature for experienced modders that allows to modify instructions in any method with pinpoint accuracy. Knowledge of the .NET CIL is required. Please consult the Harmony source code or message the author (Andreas Pardeike) if you are ready to get your hands dirty.
A patch has the ability to cancel the execution of the original method and execute another method instead. When migrating detours, it is tempting to just make a prefix patch and cancel the original method to emulate the way a detour works.
Instead, please consider each detour individually to see if it really needs to cancel the original method- chances are, this can be avoided. Avoiding canceling will improve the compatibility of your mod and make future maintenance easier.
If you really see no way around making a canceling prefix, consider messaging Andreas Pardeike on the forum or on the modder's Discord. He is the author of Harmony and has offered his help to anyone who is working on converting their detours.
There are a few different kinds of patches that can be applied to a method. They execute at different times and have different capabilities.
-
Prefix
Executes when the patched method is called, before the original code. Can modify method arguments and cancel the execution of the original method. Can be prevented from executing by prefixes from other mods on the same method. -
Postfix
Executes at the end of the of the patched method and can modify the return value. Will always execute, regardless of any prefixes on the same method. This is the preferred way if you would like to just hook into the execution flow of a method. -
Infix (or Transpiler)
Modifies the code of the patched method instruction by instruction. Neither affects nor is affected by prefixes and postfixes on the same method. This is an advanced topic and is not covered here.
This section will go through the example detours shown in the Detouring wiki page, and shows how to correctly write a patch that will achieve the same result.
Note, that if don't have a class that extends ModBase
in your assembly, you will have to call Harmony manually for your patches to be applied. Details found here.
// original method: public override void PreOpen()
[HarmonyPatch(typeof(Page_ModsConfig), "PreOpen")]
public static class ModsConfig_PreOpen_Patch {
[HarmonyPostfix]
public static void DetectModsDialogOpening(Page_ModsConfig __instance) {
Log.Message("Dialog opened: " + __instance);
}
}
In this example we created a postfix to hook into the execution of the Page_ModsConfig.PreOpen
method. Our patch will execute every time immediately after the Mods dialog opens.
Notable points:
- Patches need to be placed is separate static classes, one for each method you're patching.
- The
HarmonyPatch
annotation must be placed on the class, rather than on the method. - The patch method is annotated with
HarmonyPostfix
to indicate the type of patch we are applying. - All patch methods must be static.
- The name of the patch class and the patch method are up to you. Keep in mind that they will show up whenever HugsLib publishes a log, so it's a good idea to choose a descriptive name to indicate to other modders what your patch does.
- The comment is not required and is just there as a reference on the signature of the original method.
- Since patching is not destructive, we don't recreate the original method in our patch.
- Our parameter
__instance
is a reference to the dialog that was opened. The special name of the parameter and will always produce thethis
object when an instance method is patched. The parameter is optional.
[DetourMethod(typeof(Widgets), "ButtonText")]
public static bool ButtonText(Rect rect, string label, bool drawBackground, bool doMouseoverSound, bool active) {
const string newLabel = "Test";
return Widgets.ButtonText(rect, newLabel, drawBackground, doMouseoverSound, Widgets.NormalOptionColor, active);
}
// original method: public static bool ButtonText(Rect rect, string label, bool drawBackground = true, bool doMouseoverSound = false, bool active = true)
[HarmonyPatch(typeof(Widgets), "ButtonText", new []{typeof(Rect), typeof(string), typeof(bool), typeof(bool), typeof(bool)})]
public static class Widgets_ButtonText_Patch {
[HarmonyPrefix]
public static bool ReplaceAllButtonLabels(ref string label) {
label = "Test";
return true;
}
}
Here we create a prefix patch on Widgets.ButtonText
to replace the labels of all buttons drawn with that method.
Notable points:
- The original method is overloaded, so we need to specify the parameter types of the method we wish to patch.
- Out patch has only one parameter, and it matches the type and name of one of the parameters of the original method. We mark it as
ref
so we can modify the value passed to the original method. - Our patch has a
bool
return type. This is a requirement for prefixes. - We return
true
to allow the original method to execute. Returningfalse
would make this a canceling prefix.
// original property: public bool CanLayNow
[HarmonyPatch(typeof(CompEggLayer))]
[HarmonyPatch("CanLayNow", PropertyMethod.Getter)]
public static class EggLayer_CanLayNow_Patch {
private static readonly FieldInfo eggProgressField = AccessTools.Field(typeof(CompEggLayer), "eggProgress");
[HarmonyPostfix]
public static void ExtraFastEggs(CompEggLayer __instance, ref bool __result) {
__result = (float)eggProgressField.GetValue(__instance) * 100f > 1f;
}
}
Here we patch chickens to produce eggs at 100 times the normal rate. For the sake of simplicity, this patch does not affect the "egg progress" readout, so eggs are laid when progress is at least at 1%.
Notable points:
- We are using the
HarmonyPatch
annotation twice with different arguments. This is valid syntax and required by the Harmony library to patch properties. - Only the property getter method is patched here. To patch the setter, as well, we would have to make another patch in another static class.
- We are using reflection to access the value of a private field.
AccessTools
(part of the Harmony library) is used for convenience, butType.GetMethod
would work just as well. -
__result
is a specially named optional parameter that contains the return value of a patched method. Since we marked it asref
, we can modify the value our original method returns.
[WindowInjection(typeof(Dialog_SaveFileList_Load))]
private static void DrawSaveDialogButton(Window window, Rect inRect) {
var buttonSize = new Vector2(120f, 40f);
if (Widgets.ButtonText(new Rect(0, inRect.height - buttonSize.y, buttonSize.x, buttonSize.y), "Hello")) {
// do stuff
}
}
// orginal method: public override void DoWindowContents(Rect inRect)
[HarmonyPatch(typeof (Dialog_FileList), "DoWindowContents")]
public static class FileList_DoWindowContents_Patch {
[HarmonyPostfix]
public static void DrawHelloButton(Dialog_FileList __instance, Rect inRect) {
if (!(__instance is Dialog_SaveFileList_Load)) return;
var buttonSize = new Vector2(120f, 40f);
if (Widgets.ButtonText(new Rect(0, inRect.height - buttonSize.y, buttonSize.x, buttonSize.y), "Hello")) {
// do stuff
}
}
}
Patching is a drop-in replacement for GUI injection. Fortunately it's quite easy to update an injection to a patch- just convert it to a postfix or prefix, depending on the moment you want your controls to be drawn at.
Notable points:
- We had to patch
Dialog_FileList
instead ofDialog_SaveFileList_Load
, and then check if we are drawing inside the right window. This is becauseDialog_SaveFileList_Load
does not have it's ownDoWindowContents
method that we can patch, so we patch its parent type.
Due to the dynamic nature of canceling prefixes, currently there is no simple way to detect if another prefix has canceled the execution of your own prefix. This can be mitigated by using the HarmonyBefore
and HarmonyPriority
annotations, but mostly just by careful planning.
Designing prefixes in a way that makes their execution optional can entirely avoid the problem. Any non-optional code can always be executed in a postfix on the same method, as they are not affected by canceling prefixes.
To help track down conflicts, you can request a list of patches on a method from your HarmonyInstance
. Also, the HugsLib log publisher always includes a complete list of active patches and their respective owners.
Normally mods have to explicitly call Harmony to process their patches. HugsLib takes care of this step- all assemblies that have a class that extends ModBase
have their patches automatically detected and applied. Patching happens immediately after your ModBase
constructor has been called. See the ModBase reference if you want to disable auto-patching or access your Harmony instance.
Due to the way the Unity GUI works, we can draw a button over another button, but the first button in the stack will still receive the input event. For now a workaround would be to add both a prefix and postfix to a DoWindowContents
method. The prefix would draw an invisible button and process the clicks, while the postfix draws the button visually. The original button will still be drawn, it just won't receive the input events.
This is just a stop-gap solution until I come up with a better way to handle button replacement.
The patches listed here are just basic examples to get you up and running. The Harmony library has a lot more to offer- make sure to check out their documentation wiki for more patching goodness.