Skip to content

Core API

Tom_XV edited this page Sep 23, 2026 · 7 revisions

English | 日本語

This is the core plugin, DragNWash.ModFramework.dll, in the namespace DragNWash.ModFramework.

[BepInDependency(ModFramework.Guid, BepInDependency.DependencyFlags.HardDependency)]

ModFramework

Member What it does
const string Guid "com.tomxv.dragnwash.modframework", to depend on
const string Version The framework's version
bool IsReady True once the framework has finished starting
event Action Ready Raised once when the framework has started. If you add a handler later, it runs right away
void Register(ModInfo info) Tells the Mods screen about your mod. Registering the same GUID again replaces it
void AddModsPage(ModsScreenPage page) Adds a custom page to your mod's details. From 1.5.0 it's a tab of its own next to About and Settings; before, it was a button

ModInfo

Property Meaning
Guid Your BepInEx GUID. Required
DisplayName Name for players; it can have spaces and punctuation. Defaults to the BepInEx name
Description One or two sentences
Authors string[]
Website Project or download page
UpdateRepository Your GitHub repository as "owner/name" (core 1.1.0 and later). Once a day the core reads its latest release, and if it's newer than the installed version, the Mods screen tags the mod Update with a button to the release page. Tag your releases with the plugin's version (v1.2.0 or 1.2.0); drafts and pre-releases are never offered. Players can switch checking off
IsLibrary True for a prerequisite other mods build on. The Mods screen puts it in the Libraries group at the bottom of the list (1.5.0; before that it had a Library tag), lists the mods that need it, and asks before it's switched off
Icon A Texture2D, ideally square. Create it in Awake (see Assets). Wins over IconPath
IconPath A PNG or JPG file loaded when you register. Register from Awake
Network NetworkUse[]: every host your mod connects to (core 1.2.0, experimental). See Going online

ModsScreenPage

This is for settings that don't fit BepInEx config entries. From 1.5.0 every page a mod adds gets its own tab in the mod's details, however many there are, and Title is the tab's label. The page is built into the tab's area. If Build throws, the page shows a short message and Try again instead of half a page.

ModFramework.AddModsPage(new ModsScreenPage
{
    Guid = MyMod.Guid,
    Title = "Statistics",
    Build = panel =>
    {
        // panel is a RectTransform, cleared before each call.
        // Build it with UnityEngine.UI and TextMeshPro components.
    },
});

From 1.5.0 the panel behind a page is see-through, so text in fixed colours can get hard to read over a bright scene. ModsScreenLook has the screen's own text colours (Text, Muted, Accent, Error, Warning), which stay readable on any background, plus a Card like a setting's row and a Button like the screen's own. The colours follow the look in use (frosted glass or the tint alone), and an open page is built again when it changes, so read them while you build.

Build = panel =>
{
    RectTransform card = ModsScreenLook.Card(panel);
    TextMeshProUGUI line = new GameObject("Line").AddComponent<TextMeshProUGUI>();
    line.transform.SetParent(card, false);
    line.text = "12 cars washed";
    line.color = ModsScreenLook.Text;
    ModsScreenLook.Button(panel, "Reset", () => ResetStats());
},

A mod that should also run on an older core can reach ModsScreenLook from a method of its own marked [MethodImpl(MethodImplOptions.NoInlining)], and fall back to its own colours when that throws.

GameOptions

These are rows in the game's own Options screen, and they follow the game's flow. Changing a row previews it and shows the game's Save button, Save keeps it, Back without saving goes back to the saved value, and Set Default uses the default you give. Write labels and choices in English, and translation mods will translate them like any other UI text.

GameOptions.AddChoice(new OptionsChoice
{
    Id = "com.example.mymod.difficulty",   // unique; adding the same id again is ignored
    Label = "Difficulty",
    Choices = new[] { "Relaxed", "Normal", "Messy" },
    Section = OptionsSection.Gameplay,       // Gameplay (default), Audio or Graphics
    DefaultIndex = 1,                        // -1 leaves the row alone on Set Default
    GetSaved = () => _difficulty.Value,
    Save = index => _difficulty.Value = index,
    Preview = index => ApplyDifficulty(index), // optional, called for every shown change
});

GameOptions.AddToggle("com.example.mymod.fastmode", "Fast Mode",
    getSaved: () => _fastMode.Value,
    save: value => _fastMode.Value = value,
    preview: null, section: OptionsSection.Gameplay, defaultValue: false);

// After changing the value somewhere else (your own menu, a config file edit):
GameOptions.Refresh("com.example.mymod.fastmode");

You can call AddChoice at any time, and the row appears once the game's settings exist. It throws ArgumentException unless you give it Id, Label, at least two Choices, GetSaved and Save.

Services

This is how a library offers an API to other mods without them reaching into its internals.

// In the library (publish IScoreService in its DLL):
Services.Register<IScoreService>(new ScoreService(), new Version(1, 2), MyLibrary.Guid);

// In a mod that has a BepInDependency on the library, from Awake:
var scores = Services.Get<IScoreService>();

// In a mod without a hard dependency, or needing a minimum version:
if (Services.TryGet<IScoreService>(out var s, new Version(1, 1))) { /* use s */ }
Services.WhenAvailable<IScoreService>(s => { /* runs now, or when it is registered */ });
Member What it does
bool Register<T>(T implementation, Version version = null, string ownerGuid = null) Registers the provider of T. A second provider for the same T is refused and logged
T Get<T>() The provider, or null
bool TryGet<T>(out T service, Version minimumVersion = null) The provider if it has at least that version
Version GetVersion<T>() The version the provider registered, or null
void WhenAvailable<T>(Action<T> callback) Calls back now or as soon as T is registered

GameHooks

Use this to check that the game code you patch still exists on the running build, so a game update switches off one feature instead of crashing the game. Failed checks are logged and shown on the Mods screen as Unavailable on this game build.

// "ScrubController" stands for the game type you patch.
if (GameHooks.Require(MyMod.Guid, "Faster scrubbing", "ScrubController", "Update"))
{
    harmony.PatchAll(typeof(ScrubPatches));
}

// A check you made yourself:
var method = AccessTools.Method("ScrubController:Update");
GameHooks.Require(MyMod.Guid, "Faster scrubbing", method != null, "ScrubController.Update");
Member What it does
bool Require(string ownerGuid, string feature, string typeName, string methodName = null) True when the type (and method) exists. typeName can also be Harmony's "Type:Method" in one string, the way the Inspector's Code view copies it (core 1.2.0)
bool Require(string ownerGuid, string feature, bool passed, string detail) Records your own check and returns passed
void Unavailable(string ownerGuid, string feature, string reason) Marks a feature unavailable for some other reason (switched off after a crash, refused on this renderer); it's shown like a failed check (core 1.2.0)
IReadOnlyList<string> UnavailableFeatures(string ownerGuid) Features of a mod that failed a check

DeveloperTools (experimental, core 1.2.0)

This is one switch for everything meant for mod makers and translators rather than players, and it's off by default: Options → Mods → Drag'n Wash ModFramework → Developer tools. While it's off, the Tool window stays closed and texture reloading is refused. Put your own exports, hot reload, debug keys and windows behind it too (GUIDE rule 8).

if (DeveloperTools.Enabled) ExportWorkingCopy();
DeveloperTools.WhenEnabled(StartWatchingFiles);   // now, and each time the switch is turned on
DeveloperTools.Changed += () => { if (!DeveloperTools.Enabled) StopWatchingFiles(); };
Member What it does
bool Enabled True while developer tools are on
event Action Changed Raised when the switch changes, from the Mods screen or the config file
void WhenEnabled(Action onEnabled) Runs it now if the tools are on, and again every time they're turned on later

GameEvents and SettingMeta (experimental, core 1.2.0)

  • Game events gives you GameEvents.OnSceneLoaded, OnSceneUnloaded, OnGameStarted and OnQuitting. Each one is registered with your GUID and runs on its own.
  • With Settings meta, you put SettingMeta and SectionMeta in a ConfigDescription's tags to order the settings page, hide advanced entries and say when a restart is needed. Text values and key bindings can be edited on the page too.

Keys other settings also use

Every mod keeps its shortcut keys in its own BepInEx settings, so two mods can end up on the same key without either one knowing. The core can tell you who else is on a key. It only reports: a player may well want one key to do two things, and only they can say, so don't refuse the key or change it.

// Who else has a setting for F5, leaving your own mod out (core 1.4.2):
foreach (string other in ModFramework.WhoElseUses(KeyCode.F5, MyMod.Guid))
{
    Logger.LogInfo($"F5 is also used by {other}");   // "Drag'n Wash Localization: [Debug] DumpDialogueKey"
}

// In your own key window, under the key (1.5.0):
string note = ModFramework.SharedKeyNote(_screenshotKey);   // a ConfigEntry<KeyboardShortcut>
if (note != null)
{
    GUI.Label(noteRect, note, ToolWindow.Styles.WrappedLabel);
}

SharedKeyNote gives you the same line the Mods screen shows under a shortcut setting, like "C is also used by Screenshot key (Photo Mode). Both will answer it." With more than one other setting it ends in "All of them will answer it." instead. Each other setting goes by the name it has on its own settings page, with its mod's name in brackets when it belongs to another mod, and your own mod's other settings count too. Only the main key is compared, so Ctrl+C and C count as the same key. It's null when nobody else has the key, when the key is None, or when the setting isn't a KeyboardShortcut. The Mods screen takes its line from here as well, so a mod's own window and the Mods screen always say the same thing. The Inspector's ? panel uses it for its keys.

Member What it does
IReadOnlyList<string> WhoElseUses(KeyCode key, string exceptGuid = null) Every loaded mod with a shortcut setting on that key, as Mod: [Section] Key, leaving out exceptGuid (core 1.4.2)
string SharedKeyNote(ConfigEntryBase shortcut) The Mods screen's line for a shortcut whose key another setting also has, or null (1.5.0)

SafeFile (1.5.0)

SafeFile.Write writes a file so that a crash, or another program holding it open, can't leave it cut short halfway. Your writer fills a temporary file next to the target, and only then is it moved into place, so the file holds either the old content or the new one. If no temporary file can be opened there, it writes the target directly, which loses only that protection and none of the content. Flags and saves writes saves this way, and so does the Inspector's Export.

SafeFile.Write(path, new UTF8Encoding(false), w => w.Write(json));
Member What it does
void Write(string path, Encoding encoding, Action<StreamWriter> write) Writes path through a temporary file beside it (1.5.0)

GameInfo

Member Meaning
string UnityVersion For example 6000.3.14f1
GraphicsDeviceType GraphicsApi The graphics API in use
bool IsDirect3D12 True on Direct3D 12, where textures and fonts must be loaded at startup (see Assets)
OperatingSystemFamily OperatingSystem Windows, Linux (Steam Deck) or MacOSX

Also in the core

  • The on/off switches on the Mods screen are applied by BepInEx/patchers/DragNWash.ModFramework.Preloader.dll before plugins load.
  • Conflict detection runs after startup. The core logs game methods patched by more than one mod and marks those mods on the Mods screen. See Playing well with others.
  • On the title screen, the core shows the version and the number of loaded BepInEx plugins above the build number.

Clone this wiki locally