Skip to content

Playing well with others

Tom_XV edited this page Sep 23, 2026 · 5 revisions

English | 日本語

This page is about building a Drag'n Wash mod on the framework so it runs safely next to every other mod. The framework's first principle is that every mod runs safely together, and here's what that means for your code.

It's the guide for mod authors that used to live in the repository as docs/GUIDE.md. When another page says "GUIDE rule 11", it means rule 11 below.

Set up

Reference the framework DLLs you use (build them from the repository, or take them from a release) and declare each one as a dependency, so BepInEx loads them first and tells players when one is missing. Getting started walks through a first project step by step.

[BepInPlugin("com.example.mymod", "My Mod", "1.0.0")]
[BepInDependency(ModFramework.Guid, BepInDependency.DependencyFlags.HardDependency)]
[BepInDependency(GameText.Guid, BepInDependency.DependencyFlags.HardDependency)]
public class MyMod : BaseUnityPlugin
{
    private void Awake()
    {
        ModFramework.Register(new ModInfo
        {
            Guid = "com.example.mymod",
            DisplayName = "My Mod",
            Description = "One or two sentences on what it does.",
            Authors = new[] { "Me" },
            Website = "https://github.com/me/mymod",
            UpdateRepository = "me/mymod",
            IconPath = Path.Combine(Path.GetDirectoryName(Info.Location), "icon.png"),
        });
    }
}

If you set UpdateRepository (core 1.1.0 and later), the Mods screen tells players when your GitHub repository has a newer release than the version they're running, and opens its page. Tag releases with the plugin's version, like v1.2.0 or 1.2.0, and mark test builds as pre-releases, since those are never offered. If you don't publish releases on GitHub, leave it out.

If your mod needs a newer library than the one installed, say so with [BepInDependency(GameText.Guid, "0.1.1")]. The Mods screen then shows that version next to the library's name.

What to use

You want to Use Instead of
Tell players what your mod is ModFramework.Register(ModInfo) (Core API) nothing: plain plugins are listed too, but with less
Give players settings BepInEx Config.Bind (the Mods screen builds a page), GameOptions.AddChoice / AddToggle / AddSlider for the game's own Options screen (Core API) your own settings menu
Order your settings, hide the advanced ones, name them, say a restart is needed a SettingMeta / SectionMeta in the ConfigDescription tags (experimental, core 1.2.0; Settings meta) nothing: the page then lists them by section and key
Change text before it is shown GameText.AddRewriter (library Text; Text) patching TMP_Text.text or SetText
Know which line of dialogue is shown, and who says it GameDialogue.LineShowing, OptionShowing, TryGetLine (library Dialogue; Dialogue) patching Yarn Spinner's presenters
Find your data for a line after a game update edited it LineKey, LineResolver (library Dialogue, experimental, Dialogue 1.1.0; the "Stable line keys" section of Dialogue) keying by the English text alone
Add a debug or developer tool ToolWindow.AddTab (library Tool window; Tool window) your own OnGUI window, cursor unlocking or input blocking
Give people a command to type, or watch the log in the game ToolWindow.AddCommand, the Console tab (library Tool window, experimental, Tool window 1.1.0; Console) your own console
Show text in scripts the game's fonts lack GameFonts.Prepare and GameFonts.SetLanguage (library Assets; Assets) adding to TMP_Settings.fallbackFontAssets yourself
Load a texture or an asset bundle GameAssets.LoadTexture, GameAssets.LoadBundle (library Assets), from Awake loading while the game runs
Replace a game texture with your own, or see what is loaded assets/textures/<name>.png in your mod folder, AssetCatalog (library Assets, experimental, Assets 1.1.0; Assets) swapping textures in materials yourself
Read or change save slots and flags GameSaves, GameFlags (library Flags and saves; Flags and saves) writing savegame.dgn yourself
Share an API with other mods Services.Register<T> and Services.Get<T> (Core API) public static fields another mod has to find by reflection
Check that a game method you patch still exists GameHooks.Require (Core API) patching and hoping
See what an object, component or material holds, and try a value the Inspector tab (F1, its own library, experimental, Inspector 1.0.0) and Inspector.Inspect(target) (Inspector) a decompiler and a rebuild per guess
Run something when a scene loads, when the game has started or when it quits GameEvents.OnSceneLoaded, OnGameStarted, OnQuitting (experimental, core 1.2.0; Game events) SceneManager.sceneLoaded and Application.quitting yourself

Rules

  1. Do not patch what the framework already hooks. Text, dialogue presenters, the Options screen's settings list, GUI.Button and the game's cursor lock all have one shared hook. A second patch on the same method changes what every other mod sees. The Mods screen marks mods that patch the same game method as a Conflict (see What a Conflict means).
  2. Check before you patch. When you do need to patch a game method of your own, look it up first and call GameHooks.Require(yourGuid, "Feature name", method != null, "Type.Method"). If a game update removed it, skip that feature. The Mods screen then shows it as unavailable, and the game doesn't crash. Require also takes Harmony's "Type:Method" form as one string, like GameHooks.Require(yourGuid, "Feature name", "Yarn.Unity.LinePresenter:RunLine"). That's the form the Inspector's Code view copies, so a name you find in the game pastes straight into the check. The Code view's Patch button copies the whole patch: the check, the [HarmonyPatch] attribute (with the parameter types where the name is ambiguous), and a Prefix and Postfix with the parameters Harmony fills in.
  3. Never throw into the game. Wrap your Harmony patches in try/catch and log what goes wrong. Framework callbacks (rewriters, dialogue events, tool window tabs, option callbacks) are already isolated. If one throws, the exception is logged with your mod's GUID and the other mods keep running, but your feature stops.
  4. Load textures, fonts and bundles at startup. On Direct3D 12 with this game's Unity version, creating or uploading a texture while the game runs can crash it (Unity UUM-140564), so do it in Awake. In a tool window tab, call ToolWindow.PrepareCharacters for every non-ASCII character you draw, from Awake or Update. See Assets.
  5. Do not replace another mod's service. Services.Register refuses a second provider for the same interface. Ask for the service with Services.TryGet<T>(out var service, minimumVersion) and handle the case where it isn't there.
  6. Keep game types out of your public API. If your mod is a library for other mods, expose your own types. That way a game update changes your internals, and not every mod built on you.
  7. Do not ship the game's data unchanged. Don't put assets, script text or game DLLs in your repository or releases as they came out of the game. Anything you made by hand, or changed into something new, falls under the content policy.
  8. Keep developer features behind the developer-tools switch. Exports, hot reload, debug keys and windows should only run while DeveloperTools.Enabled is true (use DeveloperTools.WhenEnabled for features that start later), so someone who only installed a mod never sees them. Tool window tabs already work this way.
  9. Take the game's events from GameEvents. A handler on SceneManager.sceneLoaded that throws stops every mod that subscribed after it, and nobody can tell which mod it was. GameEvents.OnSceneLoaded(yourGuid, ...) runs each mod's handler on its own, names the mod on the Mods screen when it fails, and switches a handler off after three failures in a row. See Game events.
  10. Say so before you rely on reloading. A mod only gets reloaded while the game runs if it sets ModInfo.Reloadable = true (or carries [ReloadableMod]), and by doing that it promises to keep the points under Reloading your mod while the game runs.
  11. Say so before you go online. List every host your mod connects to in ModInfo.Network, with what it's for, what is sent and how to turn it off. The Mods screen shows this to players. Only send anything about the player (a name, a save, what they typed, an ID that follows them) after they turn it on. A mod that connects without saying so gets marked on the Mods screen. See Going online.

What a Conflict means

After startup, the core goes through every Harmony patch and looks for game methods that more than one mod has patched. It writes them to BepInEx/LogOutput.log:

Game methods patched by more than one mod: ...

and marks those mods Conflict on the Mods screen, with the name of the other mod and whether one of them may override the other. A conflict isn't always a bug (two postfixes on the same method can coexist), but it's the first place to look when two mods misbehave together.

To clear it, move your patch to the framework's shared hook for that thing, or patch a different method.

Reloading your mod while the game runs

This is experimental (core 1.2.0) and only works while developer tools are on. You build, and the new DLL takes the place of the running one without a restart ([Developer] WatchMods, or mods reload <guid> in the Console). Libraries are never reloaded. When your mod says it's reloadable, it promises this:

  • 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 through static fields of its own. The framework can't take out what it doesn't know about. Handlers on the libraries' events are taken out by assembly.
  • Anything it 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.

Put the build next to the installed DLL as <Mod>.dll.new, and the framework does the rest. On Windows, Mono locks the running DLL so it can't be overwritten. The game reloads from the .new file straight away, and the preloader patcher makes it the real DLL at the next launch.

<!-- 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>

Some things stay behind: the old assembly (Mono never unloads one, and it's a few hundred kilobytes per reload), and any object of the old build the game still holds. If a reload fails before the old build is taken down, the old build keeps running. If it fails after that, the log says so and you need a restart. The details are in Mod reload.

Writing a library

A library is an ordinary BepInEx plugin that other mods depend on. To make one:

  • Depend on the core, and register with IsLibrary = true, so the Mods screen shows it as a library, lists the mods that need it, and asks before it's switched off.
  • Give it its own GUID and version, and follow semantic versioning: a public API that changes in an incompatible way needs a new major version (or a new minor version while you're at 0.x, with the change written down).
  • Hook the game once, at Priority.First when the order matters, and let mods register into your hook with an explicit order.
  • Check every patch target with GameHooks.Require, and expose an IsAvailable flag so mods can tell.
  • Run each mod's callback on its own: catch its exception, log it with the mod's GUID, and carry on with the next.
  • Keep anything that touches game types internal, and offer your API as your own types or through Services.
  • When a mod is reloaded, take out what it registered with you in ModReload.Unloading (Mod reload).

Testing

  • Test with just your mod and the framework first, then alongside other mods. The Conflict tag on the Mods screen and the line "Game methods patched by more than one mod" in BepInEx/LogOutput.log tell you when you share a game method with another mod.
  • Test on Direct3D 12 (the default on Windows) and, if you can, on the Steam Deck.
  • Switch your mod off from the Mods screen and restart. The game should run without it.

Before you release

  • Go through Testing above, and read the Conflict list.
  • Declare [BepInDependency] for the core and every library you use, with a minimum version if you need a newer one: [BepInDependency(GameText.Guid, "1.0.0")].
  • If your mod goes online, make sure every host it connects to is in ModInfo.Network (rule 11).
  • Make sure nothing taken unchanged from the game is in your repository or your release (rule 7).

Clone this wiki locally