Skip to content

Mod reload

Tom_XV edited this page Sep 23, 2026 · 2 revisions

English | 日本語

Experimental. This came in with core 1.2.0 (released 2026-09-17). It's for people who build mods, and it only works while Developer tools are on.

Build the mod, close the game, start it again, click through the title screen, open the shop. That's a minute per attempt, and you make a lot of attempts in an hour. Mod reload cuts that out. After a build, the new DLL takes the place of the running one while the game keeps running. It's a developer-tools feature (Playing well with others, rule 8), so players never see it.

What can and cannot be reloaded

  • Mods (plugins): yes, when they say so (below). Everything a mod registers with the framework carries its GUID, so the old build can be taken out cleanly.
  • Libraries (the framework's Dialogue, Assets and so on, or a library of your own): no. Other mods reference a library's types directly, so after a new library build they'd still be bound to the old types. A library needs a restart.
  • Mono never unloads an assembly. A reload loads the new build next to the old one and stops the old one from doing anything. The old code stays in memory, a few hundred kilobytes per reload, which doesn't matter while you're developing.
  • Installing or updating a mod without restarting, for players: no. A mod written for "once, at startup" would break, and the Direct3D 12 problem below would land on players. Players still restart as before.
  • Only the DLL is reloaded. Textures have their own reload (Assets), and so do Drag'n Wash Localization's translation files.

Using it

  1. Turn on Developer tools: Options → Mods → Drag'n Wash ModFramework.
  2. Say that your mod is reloadable: ModInfo.Reloadable = true when you register, or a [ReloadableMod] attribute on the plugin class. A mod that says nothing is never reloaded.
  3. Build, and put the DLL next to the installed one as <Mod>.dll.new (the .csproj example below does this for you). On Windows, Mono locks the running DLL so it can't be overwritten. The framework reloads from the .new file instead, and the preloader patcher makes it the real DLL at the next launch. Where overwriting works (Linux), a changed DLL is picked up too.

Half a second after the file stops changing, the mod is reloaded, and the log (and the Console) says Reloaded <Mod> (1st time) from <file>. If there's a .pdb next to the DLL, it's loaded along with it.

Changes are picked up while the game's window is in front. Unity doesn't run the game's updates in the background, so a build that arrives while you're in another window is reloaded when you come back to the game.

Where What
[Developer] WatchMods (Mods screen: Drag'n Wash ModFramework, Reload mods when their DLL changes, under Show advanced settings; on by default) Reload a reloadable mod by itself when its DLL or .dll.new changes
mods reload <guid> in the Console Reload one mod now, whether or not WatchMods is on
mods watch on, mods watch off Switch WatchMods
mods Lists which mods are reloadable and how many times each was reloaded
The Mods screen A reloaded mod shows Reloaded with the count this session, and that the running build is not the file BepInEx loaded
<!-- In the .csproj: after a build, the DLL goes to the game as .dll.new, and the running game reloads it. -->
<PropertyGroup>
  <GameDir>C:\Program Files (x86)\Steam\steamapps\common\Drag'n Wash</GameDir>
</PropertyGroup>
<Target Name="CopyToGame" AfterTargets="Build" Condition="Exists('$(GameDir)')">
  <Copy SourceFiles="$(TargetPath)" DestinationFiles="$(GameDir)\BepInEx\plugins\$(AssemblyName)\$(AssemblyName).dll.new" />
</Target>

What happens in a reload

  1. The new build is checked first. It's loaded from bytes (so the file stays unlocked for the next build), and the framework looks for its plugin with the same GUID. If that fails, the log says why and the old build keeps running. Nothing gets taken down unless it can be replaced.
  2. The old build is taken out. Everything it registered with the framework and the libraries goes, by its GUID and by its assembly: Tool window tabs and commands, text rewriters, GameEvents handlers, handlers on the libraries' events, services, GameHooks records, its ModInfo. Its Harmony patches are removed, and its plugin component is destroyed, so its OnDestroy runs.
  3. The new build starts where BepInEx started the old one, and its Awake runs just like at startup. Its GameEvents.OnGameStarted handlers run right away, as they do for any late registration. If it adds an Options row again with the same id, that row takes over the old row's callbacks.

If a reload fails before step 2, the old build keeps running. If it fails after that, the log says so, the Mods screen marks the mod's running build unavailable, and you need a restart.

The old assembly stays behind, and so does any object of the old build the game still holds.

Direct3D 12

The new build's Awake runs mid-game, so a mod that loads textures or prepares fonts there uploads them at the very moment Direct3D 12 may crash (Unity UUM-140564). A native crash can't be caught, so the framework writes a marker file (BepInEx/config/<core GUID>.reload-in-progress) before a reload and removes it afterwards. If the marker is still there at the next start, the reload took the game down. The log says so, WatchMods is switched off until you turn it back on, and the Mods screen shows "Automatic mod reload" as unavailable. You can avoid the problem by working with -force-d3d11 in the game's launch options.

What a mod promises

If your mod says it's reloadable, it has to keep these promises:

  • Its Harmony ID is its GUID: new Harmony(MyMod.Guid). That's how its patches are found and removed.
  • It registers through the framework (ModFramework.Register, AddTab, AddCommand, AddRewriter, GameEvents, Services, GameOptions) instead of holding things in static fields of its own. The framework can't take out what it doesn't know about.
  • Only the new assembly's own statics start fresh. Anything the old build handed to the game (a coroutine, a DontDestroyOnLoad object, a file watcher) is cleaned up in OnDestroy.
  • Its Awake does nothing that's unsafe mid-game on Direct3D 12 (a texture upload), or that part is gated with GameFonts.RuntimeUploadsAreSafe.

For library authors: ModReload.Unloading

A library holds registrations from other mods, and when a mod is reloaded, its old ones have to go. ModReload.Unloading is raised just before a mod's old build is taken down, with the mod's GUID and assembly. Remove what that mod registered with you, by GUID where you kept one and by assembly otherwise.

ModReload.Unloading += (guid, assembly) =>
{
    MyLibrary.RemoveOwned(guid);                                              // what you keep by GUID
    ModReload.PruneEvent(typeof(MyLibrary), nameof(MyLibrary.Changed), assembly); // handlers on a static event
};
Member What it does
event Action<string, Assembly> Unloading Raised before a mod's old build is taken down. A handler that throws is logged; the others still run
Delegate Prune(Delegate handlers, Assembly assembly) The handlers without those whose method or target lives in that assembly
void PruneEvent(Type type, string eventName, Assembly assembly) Prune on a static event of type, by name, in place
string Reload(string guid) Queues a reload for the next frame; returns what was queued, or why not (developer tools off, not loaded, not reloadable, a library)
bool IsReloadable(string guid), IReadOnlyList<string> ReloadableMods() Whether a mod said it is reloadable; the loaded mods that did
int ReloadCount(string guid) How many times the mod was reloaded this session
bool Watching True while WatchMods is on and developer tools are on

They're all in the core, in the DragNWash.ModFramework namespace, along with ReloadableModAttribute.

Clone this wiki locally