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.
Prerequisites
Section titled “Prerequisites”-
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 populateUserLibs/(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. -
Install the .NET 6 SDK. MelonLoader IL2CPP mods target
net6.0. -
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.
Create the project
Section titled “Create the project”The fastest route is the official MelonLoader Visual Studio template:
- File → New → Project → MelonLoader IL2CPP Mod.
- 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. - 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.csprojliving 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.
Reference Anomaly
Section titled “Reference Anomaly”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.userfile or as an MSBuild property. It should point at the installed build you compile against, such asGameRoot\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.
Declare the dependency
Section titled “Declare the dependency”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.
Pick a mod id
Section titled “Pick a mod id”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 facadeClientPersistence.For("mymod").Save("loadout", myData); // compatibility facadeScheduler.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.
Project layout suggestion
Section titled “Project layout suggestion”MyMod/├── MyMod.csproj├── Core.cs ← MelonMod entry point├── Commands/│ └── HelloCommand.cs├── i18n/ ← optional, if you plan to localize│ ├── en.yaml│ └── fr.yaml└── README.mdFor 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).
Build → drop in → launch
Section titled “Build → drop in → launch”Your iteration loop is:
- Build
MyMod.dllin your IDE. - Copy it into
GameRoot/loadouts/<loadout>/local/Mods/MyMod.dll. - Launch that loadout via the Anomaly Launcher.
- 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>