Skip to main content
HarmonyLib lets you patch any .NET method at runtime. You supply a prefix (runs before the original) or a postfix (runs after the original), and Harmony rewrites the method’s IL in memory to call yours. ChroMapper’s files on disk are never modified.
Harmony patches are applied entirely in memory when your plugin loads. Unloading ChroMapper restores all methods to their original behaviour — no permanent changes are made to ChroMapper’s files on disk.

How the template initialises Harmony

The template’s Init method creates a Harmony instance keyed to your plugin’s unique ID and then calls PatchAll, which scans every type in your assembly for [HarmonyPatch] classes:
[Init]
private void Init()
{
    new Harmony(ID)
        .PatchAll(Assembly.GetExecutingAssembly());
    
    Debug.Log($"Hello from {Name}!");
}
Assembly.GetExecutingAssembly() returns your plugin’s DLL. PatchAll walks every class in it and automatically applies any it finds decorated with [HarmonyPatch]. You do not need to register patches individually.

Writing a prefix patch

A prefix runs before the original method. Returning false from a bool-returning prefix skips the original entirely; returning true (or using void) lets it proceed. The example below intercepts BeatmapObjectContainerCollection.AddObject to log every time an object is added to the map:
using HarmonyLib;
using UnityEngine;

[HarmonyPatch(typeof(BeatmapObjectContainerCollection), nameof(BeatmapObjectContainerCollection.AddObject))]
public class AddObjectPatch
{
    // Runs before BeatmapObjectContainerCollection.AddObject
    static void Prefix(BeatmapObjectData beatmapObject)
    {
        Debug.Log($"[MyPlugin] Adding object at beat {beatmapObject.time}");
    }
}
Common prefix use-cases:
  • Logging or telemetry before an action
  • Validating or modifying arguments before they reach the original method
  • Blocking the original method entirely under certain conditions

Writing a postfix patch

A postfix runs after the original method. Use the special __result parameter (prefixed with two underscores) to read or replace the return value.
using HarmonyLib;
using UnityEngine;

[HarmonyPatch(typeof(BeatmapObjectContainerCollection), nameof(BeatmapObjectContainerCollection.SpawnObject))]
public class SpawnObjectPostfix
{
    // Runs after BeatmapObjectContainerCollection.SpawnObject
    static void Postfix(BeatmapObjectContainerCollection __instance)
    {
        Debug.Log($"[MyPlugin] SpawnObject finished. Container count: {__instance.LoadedContainers.Count}");
    }
}
Common postfix use-cases:
  • Reading the return value of a method
  • Performing clean-up or notifications after an operation completes
  • Chaining additional behaviour onto an existing method

How PatchAll discovers your patches

When you call PatchAll(Assembly.GetExecutingAssembly()), Harmony looks for every class in your assembly that has at least one [HarmonyPatch] attribute and applies all prefix, postfix, and transpiler methods it finds inside them. You can have as many patch classes as you need — just add [HarmonyPatch] and they are picked up automatically on next plugin load.
When a method is overloaded, supply the parameter types to [HarmonyPatch] to select the right one:
[HarmonyPatch(typeof(SomeClass), nameof(SomeClass.SomeMethod), typeof(int), typeof(string))]
Declare a parameter named __instance with the patched type to get a reference to the object the method was called on:
static void Prefix(BeatmapObjectContainerCollection __instance)
{
    Debug.Log(__instance.name);
}

Krafs.Publicizer: accessing private and internal members

ChroMapper’s assemblies contain many internal and private types and members that you need to interact with. Normally, C# prevents you from referencing them at compile time. Krafs.Publicizer (version 2.3.0, declared in packages.config) is a Roslyn source generator that runs at build time. It produces a thin public API surface over ChroMapper’s assemblies so you can reference internal fields, methods, and types directly in your code — without reflection, without AccessTools, and with full IntelliSense support.
<!-- packages.config -->
<package id="Krafs.Publicizer" version="2.3.0" targetFramework="net48" developmentDependency="true" />
Because it is a developmentDependency, Krafs.Publicizer is consumed only at build time and produces no runtime dependency. Your output DLL does not include it, and ChroMapper does not need to have it installed.
You must restore NuGet packages before the first build. Run nuget restore in the solution directory, or let Visual Studio restore packages automatically when you open the project. See Build and deploy for details.

Build docs developers (and LLMs) love