Developers

Building a Cairn mod?

Ask questions and share what you're making with other modders.

Discord

Cairn is a Unity IL2CPP game: the C# is compiled to native code, so there's no C# assembly to decompile directly. But IL2CPP keeps full metadata — every type name, method name, field offset, and function address survives. Reverse engineering Cairn means recovering that metadata, reading the native code with names applied, and hooking it at runtime with MelonLoader + Harmony.

You end up with two views of every method:

The C# tells you what exists; the native tells you what it does.

Tools

Il2CppDumper Recover names, offsets, and addresses from the game's metadata
Ghidra Decompile native method bodies
ILSpy / ilspycmd Read the C# view
MelonLoader Mod loader; also generates the C# proxy assemblies you build against
Il2CppInterop Bridges your .NET code to the IL2CPP runtime (ships with MelonLoader)
Harmony (0Harmony) Runtime method patching (ships with MelonLoader)
UnityExplorer (Il2CppInterop fork) Live scene/object inspector in-game
CairnAPI Named APIs over the game's internals — the interop already done (a mod you reference)
CairnDevTools HTTP console that evals C# against the live game

1. Install MelonLoader

Run the MelonLoader installer against Cairn.exe, then launch the game once. First launch generates:

2. Dump the metadata

Il2CppDumper.exe "<game>\GameAssembly.dll" "<game>\Cairn_Data\il2cpp_data\Metadata\global-metadata.dat" dump

It ends on a "press any key" prompt that reports a nonzero exit — the output is fine.

You get:

To find a method's address: grep dump.cs for the type, read the // RVA: 0x... comment above the method. Its address in Ghidra is 0x180000000 + RVA.

3. The C# view

Open any Il2CppAssemblies\*.dll in ILSpy. The game's own code is in the Il2CppTheGameBakers.Cairn.* assemblies; global-namespace game types appear under the Il2Cpp namespace.

ilspycmd can choke on a whole assembly — decompile per-type: ilspycmd -t Full.Type.Name <dll>.

4. The native view (Ghidra)

GameAssembly.dll is large (~90 MB). Full auto-analysis takes hours and you don't need it — import once, apply names, decompile only the functions you care about.

  1. Create a project, import GameAssembly.dll with analysis off (-noanalysis headless, or uncheck analyzers in the GUI).
  2. Apply names: Il2CppDumper ships ghidra_with_struct.py — run it and point it at script.json. It labels every function and creates the metadata structs. Note: Ghidra 12 removed Jython, so the stock script needs Ghidra ≤11.x; on 12+ run it under PyGhidra or port the apply loop (it's a small loop over script.json entries — name + create function at each address).
  3. Decompile targets on demand: navigate to 0x180000000 + RVA (from dump.cs) and read the decompiler output, or run a headless post-script that decompiles a list of addresses to .c files. Targeted decompiles of a handful of methods take a couple of minutes; that's the loop.

Reading tips:

5. Write a mod

A mod is a net6.0 class library referencing MelonLoader and the proxy assemblies, with Private=false on everything (the game already has them):

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <GameDir Condition="'$(GameDir)' == ''">C:\Program Files (x86)\Steam\steamapps\common\Cairn</GameDir>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="MelonLoader"><HintPath>$(GameDir)\MelonLoader\net6\MelonLoader.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="0Harmony"><HintPath>$(GameDir)\MelonLoader\net6\0Harmony.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Il2CppInterop.Runtime"><HintPath>$(GameDir)\MelonLoader\net6\Il2CppInterop.Runtime.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Il2Cppmscorlib"><HintPath>$(GameDir)\MelonLoader\Il2CppAssemblies\Il2Cppmscorlib.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="Il2CppTheGameBakers.Cairn.Global"><HintPath>$(GameDir)\MelonLoader\Il2CppAssemblies\Il2CppTheGameBakers.Cairn.Global.dll</HintPath><Private>false</Private></Reference>
    <Reference Include="UnityEngine.CoreModule"><HintPath>$(GameDir)\MelonLoader\Il2CppAssemblies\UnityEngine.CoreModule.dll</HintPath><Private>false</Private></Reference>
    <!-- add Il2CppAssemblies references as the compiler asks for them -->
  </ItemGroup>
  <Target Name="InstallToGame" AfterTargets="Build">
    <Copy SourceFiles="$(TargetPath)" DestinationFolder="$(GameDir)\Mods" />
  </Target>
</Project>

Entry point:

using MelonLoader;

[assembly: MelonInfo(typeof(MyMod.Core), "MyMod", "0.1.0", "you")]
[assembly: MelonGame("The Game Bakers", "Cairn")]

namespace MyMod;

public class Core : MelonMod
{
    public override void OnInitializeMelon() => LoggerInstance.Msg("hello");
    public override void OnUpdate() { }   // per-frame
}

Build, launch, check <game>\MelonLoader\Latest.log. Note MelonLoader holds mod DLLs open while the game runs — kill the game before rebuilding.

Before writing raw interop against a game system, check CairnAPI — menus and mod screens, prompts, inventory, teleport, game-state events, already reverse-engineered and wrapped. Reference Mods\CairnAPI.dll like the assemblies above (Private=false). CairnModOptions gives your mod a page in the game's Settings menu — declare typed options, write no UI.

6. Harmony patching

Patch the game's methods through the proxy types:

using HarmonyLib;
using Il2Cpp;   // global-namespace game types land here

[HarmonyPatch(typeof(SomeGameType), nameof(SomeGameType.SomeMethod))]
static class SomeMethod_Patch
{
    static bool Prefix(SomeGameType __instance) => true;   // false skips the original
    static void Postfix(SomeGameType __instance, ref int __result) { }
}

MelonMod patches everything in the assembly automatically; or call HarmonyInstance.PatchAll() / HarmonyInstance.Patch(...) yourself.

IL2CPP-specific gotchas:

7. Il2CppInterop gotchas

The proxy layer is mostly transparent, until it isn't:

8. Live inspection

Install the Il2CppInterop/CoreCLR fork of UnityExplorer as a mod — in-game scene graph browser, component inspector, C# REPL. For "what object is this / what's this field right now" questions it's faster than any amount of decompiling. Pair it with the decompiles: the scene dump shows you the objects, the native code shows you who writes them.

CairnDevTools goes further: an HTTP console (ports 14200+) that compiles and evals C# against the live game. POST a script, get the result — probe fields, call methods, drive the game to a state, all without a rebuild. It's the fastest hypothesis-tester in the kit.

The loop

Everything above compresses into one working rhythm. Say you want to change how some system behaves:

  1. Find it. Grep dump.cs for the game's own vocabulary — climbing, stamina, rope, whatever you're after. That names the types and hands you every field offset and method address involved.
  2. Get the shape. Open those types in ILSpy: signatures, enums, which class holds which state.
  3. Get the truth. Decompile the methods in Ghidra — and their callees, gates, and overrides. A mechanism is never one method; stop when you can narrate the whole path from input to effect.
  4. Check it live. UnityExplorer or a CairnDevTools eval: does that field really hold what you think, on the object that's actually in the scene?
  5. Hook it. A Harmony patch at the narrowest point that changes the behavior — and drive the game through its own methods rather than reimplementing them.
  6. Prove it. Log from the patch, launch, watch it fire. Untested patches silently miss (inlining, wrong override) more often than they fail loudly.

Each pass through the loop is cheap — minutes, not hours. When a change misbehaves, the answer is almost always another lap: decompile one more caller, eval one more field.