Skip to content

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 OnLoad throws), 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:

csharp
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:

csharp
[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:

ParameterValue
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
__instanceThe instance the method was called on
__resultThe 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:

csharp
[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 (__instance on a static method, __result on a void method, or ___field for 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:

csharp
[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.String instead of string, Il2CppSystem.Collections.Generic.List<T> instead of List<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().

Mods are third-party software. Lustral does not verify their behavior and is not responsible for any damage they cause.