Harmony Patching
Mods modify game behavior by patching the game's methods. Lustral includes HarmonyX, a fork of Harmony, on both scripting backends. Mod projects reference it automatically; do not add a separate package reference or ship its assembly.
The Mod's Harmony Instance
Mod.Harmony is a Harmony instance identified by the mod's ID. Patches applied through it are associated with the mod:
- If the mod fails to load (its constructor or
OnLoadthrows), Lustral removes its patches. - Crash reports list which mod patched which method.
Apply all patch classes in the assembly from OnLoad, so that the patches are in place before the game's code runs:
public override void OnLoad() => Harmony.PatchAll(typeof(ModEntry).Assembly);Lustral does not apply patches automatically; nothing is patched until the mod calls PatchAll or Patch.
Patch Classes
A patch class specifies the target method and contains the patch methods:
[HarmonyPatch(typeof(PlayerController), nameof(PlayerController.TakeDamage))]
internal static class TakeDamagePatch
{
// Runs before the original method. Returning false skips the original and any remaining prefixes.
private static bool Prefix(PlayerController __instance, ref int damage)
{
if (ModEntry.Instance.Settings.HalfDamage)
damage /= 2;
return true;
}
// Runs after the original method, even if a prefix skipped it.
private static void Postfix(PlayerController __instance, int __result)
{
ModEntry.Instance.Log.Debug($"{__instance.name} took damage, {__result} health left");
}
}Patch method parameters are matched by name:
| Parameter | Value |
|---|---|
A parameter of the original method, by name (damage) | Its value; declare it ref to modify it |
__0, __1, ... | The original method's parameters by position |
__instance | The instance the method was called on |
__result | The return value (declare it ref in a postfix to modify it) |
___fieldName (three underscores) | A field of the instance; declare it ref to modify it |
Harmony's documentation covers further topics: transpilers, finalizers, TargetMethod for targets that [HarmonyPatch] cannot express, and manual patching with Harmony.Patch.
Overloaded Methods
For overloaded methods, specify the parameter types of the target overload:
[HarmonyPatch(typeof(Inventory), nameof(Inventory.Add), typeof(Item), typeof(int))]Compile-Time Validation
Lustral's analyzers validate [HarmonyPatch] classes against the game's assemblies during the build, using the same resolution rules as Harmony at runtime:
- LUS0101: the target method does not exist, for example because of a typo or a game update. Reported for IL2CPP games, whose interop assemblies expose every member; on Mono games, private members are not visible to the compiler and are not reported.
- LUS0102: the target method is overloaded and the patch does not specify an overload.
- LUS0103: a patch parameter matches neither a parameter of the target method nor one of Harmony's special parameters (
__instanceon a static method,__resulton avoidmethod, or___fieldfor a field that does not exist).
Private Members
nameof can only reference accessible members. Specify private methods by name, and use Harmony's AccessTools or Traverse to access private fields and methods from mod code:
[HarmonyPatch(typeof(PlayerController), "RecalculateSpeed")]On IL2CPP games, the interop assemblies expose private members as public, so nameof can be used.
IL2CPP Considerations
Patching works the same way on IL2CPP games, with the following differences resulting from the game's native code:
- Patch parameters use interop types:
Il2CppSystem.Stringinstead ofstring,Il2CppSystem.Collections.Generic.List<T>instead ofList<T>. Strings convert implicitly in both directions. See Mono and IL2CPP. - Inlined methods: the IL2CPP compiler inlines small methods. A patch on an inlined method only runs when it is called from code where it was not inlined. If a patch on a short method, such as a property getter, never runs, patch its callers instead.
- Transpilers are not supported, since the game contains native code rather than IL. Use prefixes and postfixes.
Removing Patches
Patches remain active for the entire session. To remove all patches applied by the mod, call Harmony.UnpatchSelf().