-
-
Notifications
You must be signed in to change notification settings - Fork 2
Playing well with others
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.
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.
| 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 |
-
Do not patch what the framework already hooks. Text, dialogue presenters, the Options screen's settings list,
GUI.Buttonand 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). -
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.Requirealso takes Harmony's"Type:Method"form as one string, likeGameHooks.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. -
Never throw into the game. Wrap your Harmony patches in
try/catchand 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. -
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, callToolWindow.PrepareCharactersfor every non-ASCII character you draw, fromAwakeorUpdate. See Assets. -
Do not replace another mod's service.
Services.Registerrefuses a second provider for the same interface. Ask for the service withServices.TryGet<T>(out var service, minimumVersion)and handle the case where it isn't there. - 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.
- 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.
-
Keep developer features behind the developer-tools switch. Exports, hot reload, debug keys and windows should only run while
DeveloperTools.Enabledis true (useDeveloperTools.WhenEnabledfor features that start later), so someone who only installed a mod never sees them. Tool window tabs already work this way. -
Take the game's events from
GameEvents. A handler onSceneManager.sceneLoadedthat 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. -
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. -
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.
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.
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
DontDestroyOnLoadobject, a file watcher) is cleaned up inOnDestroy. - Its
Awakedoes nothing that's unsafe mid-game on Direct3D 12 (a texture upload), or that part is gated withGameFonts.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.
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.Firstwhen the order matters, and let mods register into your hook with an explicit order. - Check every patch target with
GameHooks.Require, and expose anIsAvailableflag 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 throughServices. - When a mod is reloaded, take out what it registered with you in
ModReload.Unloading(Mod reload).
- 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.logtell 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.
- 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).
Players
Mod authors
- Getting started
- Playing well with others (the guide)
- Going online
- Installer
- Mod reload
- Overrides (no code)
- Graphs (no code, makes things happen)
Tools (F1, developer tools)
- Inspector
- Console
- Code graph
- Bridge (AI clients, MCP)
API
日本語
- ホーム
- プレイヤー向け · ランチャー · FAQ · クラッシュレポート
- はじめての Mod · ほかの Mod と一緒に動かす · 外と通信する Mod · インストーラー · Mod の再読み込み · Overrides · Graphs
- Inspector · Console · コードのグラフ · Bridge
- 中核 API · 操作の登録簿 · Text · Dialogue · Tool window · Assets · Flags and saves · GameEvents · SettingMeta
Links
- Repository
- Releases
- Changelog
- Design records: DESIGN · ROADMAP · CONTENT_POLICY