Skip to content

Environment Setup

Writing a MelonLoader mod for Anomaly is writing a standard IL2CPP MelonMod plus three small conventions: the correct DLL references, the load-order attribute, and a namespace-shaped mod id. This chapter gets you from a clean IDE to a build-ready project template.

  1. Install the Anomaly Launcher and run the modded game once. MelonLoader needs its first run to generate Il2CppAssemblies/ (the Il2Cpp-to-CLR wrappers) and to populate UserLibs/ (the shared libraries). Anomaly cannot ship these for you — they’re generated against the specific SCP:SL build you have installed. See Install the Modded Client if you haven’t done this yet.

  2. Install the .NET 6 SDK. MelonLoader IL2CPP mods target net6.0.

  3. Set up your IDE. Visual Studio, JetBrains Rider, and VS Code all work. Visual Studio has a MelonLoader template that is the fastest path — see melonwiki.xyz for installation.

The fastest route is the official MelonLoader Visual Studio template:

  1. File → New → Project → MelonLoader IL2CPP Mod.
  2. Point the wizard at your modded SCPSL.exe. The template inspects the binary, fills in the right target framework, and pre-populates references to MelonLoader itself.
  3. Save the project in a working directory separate from the GameRoot. You’ll copy the build output into a loadout’s local/Mods/ folder as part of your iteration loop; you don’t want the .csproj living inside the game directory.

If you’re using Rider or VS Code, clone an existing minimal mod repository as a starting point (the MelonLoader wiki links several), then adjust the references below.

Add explicit references to the Anomaly public assemblies:

<ItemGroup>
<Reference Include="Anomaly.Client.Api">
<HintPath>$(GameVersionDir)\UserLibs\Anomaly.Client.Api.dll</HintPath>
<Private>false</Private>
</Reference>
<Reference Include="Anomaly.Shared">
<HintPath>$(GameVersionDir)\UserLibs\Anomaly.Shared.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>
  • Define $(GameVersionDir) in a local .csproj.user file or as an MSBuild property. It should point at the installed build you compile against, such as GameRoot\games\14.2.6.
  • <Private>false</Private> tells MSBuild not to copy the DLLs next to your output. Anomaly provides them at runtime — you’re compiling against their public surface, not distributing them.

MelonLoader calls OnInitializeMelon in dependency order. If your mod calls any Anomaly API during OnInitializeMelon (registering a command, subscribing to an event, etc.), you must declare the dependency:

using MelonLoader;
[assembly: MelonInfo(typeof(MyMod.Core), "MyMod", "0.1.0", "YourName", null)]
[assembly: MelonGame("Northwood", "SCPSL")]
[assembly: MelonGameVersion("14.2.6")]
[assembly: MelonAdditionalDependencies("Anomaly")]

The string "Anomaly" must match Anomaly.Client’s own MelonInfo name. Without this attribute, load order is undefined — your mod may hit Anomaly’s APIs before Anomaly’s own OnInitializeMelon has finished. Symptoms range from silent no-ops to NullReferenceException deep inside the registry code.

Mods that only call Anomaly APIs from event handlers (i.e. not during OnInitializeMelon) technically don’t need the attribute, but adding it is free and defends you against future refactors where you start using an Anomaly API earlier than you planned.

Your mod’s id is a lowercase, dotted namespace:

regex: ^[a-z][a-z0-9_]{0,31}(\.[a-z][a-z0-9_]{0,31}){0,39}$

Valid: mymod, acme.wargames, team_alpha.ui, contoso.supportbots.v2.

Invalid: MyMod (uppercase), 123mod (starts with digit), anomaly.foo (reserved prefix), my-mod (hyphens not allowed), launcher.x (also reserved).

The id is what you pass to every Anomaly registry that takes a modId or a namespaced key:

CommandRegistry.Register("mymod", new MyCommand());
InputRegistry.Register(new InputBindingDefinition { Id = "mymod.jump", ... });
MelonPreferences.CreateCategory("mymod", "MyMod");
ClientConfig.For("mymod").Set("volume", 0.75f); // compatibility facade
ClientPersistence.For("mymod").Save("loadout", myData); // compatibility facade
Scheduler.StartCooldown("mymod.ability", TimeSpan.FromSeconds(5));
Tr.RegisterMod("mymod");

There is no “register my mod” call — the id is simply a required prefix everywhere it’s used. Reserved prefixes (anomaly, anomaly.*, launcher, launcher.*) are rejected by the registries.

MyMod/
├── MyMod.csproj
├── Core.cs ← MelonMod entry point
├── Commands/
│ └── HelloCommand.cs
├── i18n/ ← optional, if you plan to localize
│ ├── en.yaml
│ └── fr.yaml
└── README.md

For a shared-protocol mod (see Architecture Choices), add a separate MyMod.Shared project targeting net48 that mirrors the Anomaly.Shared constraints (no UnityEngine, no Mirror, no MelonLoader).

Your iteration loop is:

  1. Build MyMod.dll in your IDE.
  2. Copy it into GameRoot/loadouts/<loadout>/local/Mods/MyMod.dll.
  3. Launch that loadout via the Anomaly Launcher.
  4. Watch the MelonLoader console (enable it in MelonLoader preferences if you haven’t).

A post-build step that copies your DLL straight to a dev loadout is a common productivity win:

<Target Name="CopyToMods" AfterTargets="Build">
<Copy
SourceFiles="$(TargetPath)"
DestinationFolder="$(GameRoot)\loadouts\$(LoadoutName)\local\Mods" />
</Target>

Client Mod Quickstart.