A Rain World code mod begins with a class derived from BepInEx.BaseUnityPlugin. The BepInEx.BepInPlugin attribute gives BepInEx the plugin GUID, display name, and version. In the repository example, the GUID is rainworldmodding.firstplugin.

using BepInEx;
 
[BepInPlugin("rainworldmodding.firstplugin", "First Plugin", "0.1.0")]
public sealed class FirstPlugin : BaseUnityPlugin
{
    private void OnEnable()
    {
        Logger.LogInfo("First Plugin enabled");
    }
 
    private void OnDisable()
    {
        Logger.LogInfo("First Plugin disabled");
    }
}

The complete FirstPlugin source is in the repository. Its modinfo.json uses the same identifier as the plugin GUID. Keeping those identifiers aligned makes the relationship between the Remix entry, folder, and assembly clear.

What these callbacks establish

BepInEx.Bootstrap.Chainloader.Start() creates plugin components with Unity’s GameObject.AddComponent. Unity then invokes OnEnable when the component becomes enabled. At that point, the plugin may log and subscribe to hooks. This callback does not prove that a gameplay room exists or that post-initialization configuration and user data have loaded.

OnDisable is the matching place to remove ordinary gameplay hooks. The first example only logs, so it has nothing to unsubscribe. A plugin that subscribes to On.Player.Update should remove the same named delegate in OnDisable. Hook lifecycle shows that pattern and explains why Rain World’s mod initialization needs separate treatment.

Build the repository example

Install a .NET SDK, rather than only the .NET runtime, and clone this wiki repository. The build needs a local Rain World installation containing BepInEx 5 and BepInEx/plugins/HOOKS-Assembly-CSharp.dll.

From the repository root, build the included SDK-style project. Replace the path with the folder Steam opens from Rain World’s Installed Files page.

dotnet build examples/FirstPlugin/FirstPlugin.csproj -p:RainWorldPath="/full/path/to/Rain World"

On Windows, Linux, and macOS, dotnet build accepts the same project and property syntax. Each shell has its own syntax for setting a persistent or temporary environment variable. Project file gives the full .csproj, platform-specific examples, all local references, and the Private=false setting that keeps installed DLLs out of the build output.

The checked project targets net472 and writes examples/FirstPlugin/bin/Debug/net472/FirstPlugin.dll. This source compiled on Windows with .NET SDK 10.0.400 against Rain World v1.11.8, Steam build 22785462, BepInEx 5.4.17.0, and the installed HOOKS-Assembly-CSharp.dll. It does not copy the DLL into the game or start Rain World.

The older examples/build.ps1 command remains available for the other repository examples. It is a PowerShell route. Use the project-file route for a portable build command. Operating systems explains why portable compilation does not prove game runtime support outside Windows.

Package it as a local mod

BepInEx.MultiFolderLoader.ModManager.AddMod(System.String) checks a mod’s <CurrentGameVersion>/plugins directory first, then newest/plugins, then the plain plugins directory. BepInEx.MultiFolderLoader.ModManager.GetPluginDirs() returns the selected non-null plugin paths for discovery. The plain layout established for this example is:

RainWorld_Data/StreamingAssets/mods/rainworldmodding.firstplugin/
├── modinfo.json
└── plugins/
    └── FirstPlugin.dll

Place the built DLL under mods/<id>/plugins, alongside the mod metadata at the mod root. The example repository contains matching metadata. Version and newest subfolders are not needed for this simple layout. Exact copying, Remix enablement, restart behavior, and loading of this example have not been tested in the game.

BOI’s maintainer limits it to Rain World 1.5, so it is not a prerequisite for this build. The baseline used here is Rain World v1.11.8. Runtime reference records the game build, assembly hashes, bundled module manifests, and the boundary of the verification.

First game check

After making a separate copy of the DLL in a local mod folder and enabling the entry in Remix, check these outcomes in a controlled test:

  1. The log contains First Plugin enabled once after the plugin is enabled.
  2. The loader reports no exception for the plugin.
  3. Disabling or shutting down produces First Plugin disabled where the installed loader supports that transition.
  4. A second launch does not create duplicate subscriptions once hooks are added.

Compilation verifies the C# syntax and the installed assembly signatures. It does not verify these game outcomes. Once this shell is loading, continue with Object interaction for a bounded On.Player.Update example, or Rooms and regions for content that begins with an Arena room instead of a gameplay hook.

Sources