Using preloader patchers

Preface

As of version 4.0, BepInEx allows to write preload-time patchers that modify assemblies before the game loads them.
While most plug-ins can use Harmony to do runtime patching, using preload-time patchers provides more fine control over how the assembly is patched.

It is still recommended that you use Harmony wherever possible because Harmony makes sure all patches are compatible with each other. Use Mono.Cecil only if something cannot be done by Harmony (more info below).

Difference from runtime patchers

Because preload-time patchers are run before the assemblies are loaded into memory, the patchers have more fine-grained control over how to modify the assemblies.

Feature Preload-time patcher Runtime patcher
Used library Mono.Cecil Harmony
Used contract Written in a separate DLL, uses a special contract Written in plug-in DLL, uses Harmony's API
Application time Applied on raw assemblies before the game initializes Applied on assemblies already loaded in memory
Can apply hooks Yes Yes, as long as the target is not inlined by JIT
Can rewrite methods' IL Yes Yes
Can modify field/method propeties Everything Partially
Can add new classes, methods and fields Yes No
Can replace assemblies Yes No

Thus, use preload-time patchers only if you must modify the structure of the assembly. For hooking methods use Harmony.

Warning

Preloader-time patching comes with its own caveats! Refer to the notes below for more information.

Writing a patcher

Requirements

Assuming you know how to use an IDE of your choice, you will need to

Patcher contract

BepInEx considers a patcher any class that has the following members:

Here is an example of a valid patcher:

using System.Collections.Generic;
using Mono.Cecil;

public static class Patcher
{
    // List of assemblies to patch
    public static IEnumerable<string> TargetDLLs { get; } = new[] {"Assembly-CSharp.dll"};

    // Patches the assemblies
    public static void Patch(AssemblyDefinition assembly)
    {
        // Patcher code here
    }
}

Specifying target DLLs

To specify which assemblies are to be patched, create a public static IEnumerable<string> TargetDLLs getter property.

Note that TargetDLLs is enumerated during patching, not before. That means the following enumerator is valid:

public static IEnumerable<string> TargetDLLs => GetDLLs();

public static IEnumerable<string> GetDLLs()
{
    // Do something before patching Assembly-CSharp.dll

    yield return "Assembly-CSharp.dll";

    // Do something after Assembly-CSharp has been patched, and before UnityEngine.dll has been patched

    yield return "UnityEngine.dll";

    // Do something after patching is done
}

Patch method

A valid patcher method has one of the following signatures:

public static void Patch(AssemblyDefinition assembly);
public static void Patch(ref AssemblyDefinition assembly);

In the latter case, the reference to the AssemblyDefinition is passed. That means it is possible to fully swap an assembly for a different one.

Patcher initialiser and finaliser

In addition, the patchers are allowed to have the following methods:

// Called before patching occurs
public static void Initialize();

// Called after preloader has patched all assemblies and loaded them in
// At this point it is fine to reference patched assemblies
public static void Finish();

Logging

BepInEx allows to either use the Standard Output (provided through Console class) or -- more fittingly -- the methods provided by System.Diagnostics.Trace class.

With BepInEx 5 you can also use CreateLogSource(String) to use BepInEx's own logging system.

Deploying and using

Build the project as a separate DLL from the plug-in. Place the DLL in BepInEx/patchers and run the game.

Notes and tips