Modding:Monkey patching: Difference between revisions
(Made heading level more consistent with other pages) |
(→Basic Patching: prevent the patches from getting double applied) |
||
Line 26: | Line 26: | ||
public void Start(ICoreAPI api) { | public void Start(ICoreAPI api) { | ||
this.api = api; | this.api = api; | ||
harmony = new Harmony( | // The mod is started once for the server and once for the client. | ||
// Prevent the patches from being applied by both in the same process. | |||
if (!Harmony.HasAnyPatches(Mod.Info.ModID)) { | |||
harmony = new Harmony(Mod.Info.ModID); | |||
harmony.PatchAll(); // Applies all harmony patches | |||
} | |||
} | } | ||
// ... Harmony patch functions here | // ... Harmony patch functions here |
Revision as of 16:47, 27 January 2024
This page was last verified for Vintage Story version 1.18.
Monkey patching (changing code at runtime) in Vintage Story is available through Harmony.
With Harmony you can change methods, constructors and properties.
Full documentation on harmony is available here..
It is recommended that you use dnSpy on any dlls or mods you wish to patch, rather than viewing the source code on GitHub. This is because there is often more in the dlls than can be found on GitHub. On Linux, this executable can be run through wine.
Getting Started
To use Harmony in you Vintage Story mod you first have to reference the "0Harmony.dll" file in the Vintage Story "Lib" directory (VintageStory\Lib\0Harmony.dll) in your project.
Then, in whatever Start()
method you use, you have to create a Harmony-instance by calling the constructor with your unique identifier, preferrably your modid (this lets harmony differentiate between different patches and allows other people to patch your patches).
var harmony = new Harmony("MyModId");
Now you are all set to patch everything inside the game like described in the documentation.
Basic Patching
The simplest and easiest way to patch code is to run harmony.PatchAll()
after initializing harmony with the constructor, and to put your patches into various "multi-patch classes". To mark a class as one of these multi-patch classes, put the [HarmonyPatch]
annotation at the top.
[HarmonyPatch] // Place on any class with harmony patches
public class MyModSystem : ModSystem {
public ICoreAPI api;
public Harmony harmony;
public void Start(ICoreAPI api) {
this.api = api;
// The mod is started once for the server and once for the client.
// Prevent the patches from being applied by both in the same process.
if (!Harmony.HasAnyPatches(Mod.Info.ModID)) {
harmony = new Harmony(Mod.Info.ModID);
harmony.PatchAll(); // Applies all harmony patches
}
}
// ... Harmony patch functions here
Inside a multi-patch class, add [HarmonyPrefix]
, [HarmonyPostix]
, and [HarmonyPatch]
annotations to tell functions to run before or after a certain in-game function is run. A prefix will run the function before the original function specified by [HarmonyPatch]
, while a postfix will run the function after it. These functions should be marked public static void
or, if you're writing a prefix and would like to prevent the original function from running, use public static bool
and return false
to cancel the original (return true
to run the original anyway).
Every patch function can be given parameters with the same names as the original, or special names with double underscores at the start. For a full list of special names parameters can have, see this page of the harmony documentation.
This function will print "{item stack} is about to take {damage} damage!" to the console whenever an item entity in the world is about to receive damage.
[HarmonyPrefix]
[HarmonyPatch(typeof(EntityItem), "ReceiveDamage")]
// Note that the name of the function does not matter
public static void ItemReceivedDamage(EntityItem __instance, float damage) { // For methods, use __instance to obtain the caller object
api.Logger.Event("{0} is about to take {1} damage!", __instance.Slot.ItemStack, damage);
}
Changing it to a postfix will instead run the function after the item entity has taken the damage.
[HarmonyPostfix]
[HarmonyPatch(typeof(EntityItem), "ReceiveDamage")]
public static void ItemReceivedDamageAfter(EntityItem __instance, float damage) {
api.Logger.Event("{0} took {1} damage!", __instance.Slot.ItemStack, damage);
api.Logger.Event("Is it still alive? {0}", __instance.Alive);
}
Or, by changing the prefix to return false, we can make items never take damage.
[HarmonyPrefix]
[HarmonyPatch(typeof(EntityItem), "ReceiveDamage")]
// Parameters are not explicitly required.
public static bool ItemReceivedDamageCancel() {
return false;
}
Transpilers
Transpilers are a special kind of patch that allow you to change a small part of the code of a function, without canceling the whole thing with a prefix and rewriting it.
To understand transpilers, you first have to understand IL code. IL code is what C# compiles into, and is what is stored and run in any .NET executable or .NET dll files. It's an assembly-like language which uses a stack to manage data. Instructions generally will pop data from the stack, perform some calculation on that data, then push the result back onto the stack. For a full list of IL instructions, look at the MS docs.
To view the IL code of any in-game function, use a disassembler such as dnSpy. When you know what instruction(s) you want to change, add, or delete, you'll want to use the [HarmonyTranspiler]
annotation in combination with a CodeMatcher
object to locate and modify the IL code. You could also yield return
instructions manually, though that is far more time consuming. For a full list of CodeMatcher methods, see the harmony documentation on the CodeMatcher object.
This method patches the crash handler to include a little watermark.
[HarmonyTranspiler]
[HarmonyPatch(typeof(CrashReporter), "Crash")]
public static IEnumerable<CodeInstruction> Crash(IEnumerable<CodeInstruction> instructions, ILGenerator generator) {
return new CodeMatcher(instructions, generator)
// This method finds the first time a {ldstr "Game Version: "} instruction occurs.
// It then places the cursor at the start of it.
.MatchStartForward(new CodeMatch(OpCodes.Ldstr, "Game Version: "))
// This method replaces the operand of the instruction that starts at the cursor.
// Specifically, it replaces the string "Game Version: " with a new string including some extra text.
.SetOperandAndAdvance("Hello from the wiki!" + Environment.NewLine + "Game Version: ")
// This method finalizes our changes with the methods above.
.InstructionEnumeration();
}
Cleaning Up
For good practice, if you patch the game with your mod, make sure you unpatch it when your mod unloads. The easiest way to do this is to run harmony.UnpatchAll("MyModId")
in the Dispose
method of a ModSystem. Make sure you include the id in the call to UnpatchAll()
, as otherwise you may accidentally unpatch other mods before they expect.
public void Dispose() {
harmony.UnpatchAll("MyModId");
}
}
Remarks
- Just because you can use monkey patching, does not mean you should. Patches are often fragile and break easily (especially if you access internal private methods). So if you can use normal means of modding you probably should.
- You can't use harmony to patch fields (because they are not called), so in order to change field access you have to patch all calls to this field with patches. You can patch getters and setters, if they are available.
Wondering where some links have gone?
The modding navbox is going through some changes! Check out Navigation Box Updates for more info and help finding specific pages.
Modding | |
---|---|
Modding Introduction | Getting Started • Theme Pack |
Content Modding | Content Mods • Developing a Content Mod • Basic Tutorials • Intermediate Tutorials • Advanced Tutorials • Content Mod Concepts |
Code Modding | Code Mods • Setting up your Development Environment |
Property Overview | Item • Entity • Entity Behaviors • Block • Block Behaviors • Block Classes • Block Entities • Block Entity Behaviors • Collectible Behaviors • World properties |
Workflows & Infrastructure | Modding Efficiency Tips • Mod-engine compatibility • Mod Extensibility • VS Engine |
Additional Resources | Community Resources • Modding API Updates • Programming Languages • List of server commands • List of client commands • Client startup parameters • Server startup parameters Example Mods • API Docs • GitHub Repository |