Jump to content

Mod Loader/Reference/Plugin compatibility APIs

From Threadbox

Use these APIs to register your plugin, share game controls, manage resources, and keep data with a save. For a complete startup example, see Start and stop a plugin.

Examples using shop layouts and owned registrations require Mod Loader 0.7.7 or later. Check the relevant capability before using optional support. Current readiness and failure-handling examples are in the lifecycle guide.

Share shop layout information

Check ModLoaderApi.Supports("shop-layout") before using ModShopApi. Register a provider with an owned ModContext. Higher priorities win; ties use plugin ID and local registration ID. Return null to leave a shop to another provider. Layouts describe column-major fabric rows (1 to 32). Preloading uses the selected layout and still counts neighboring fabrics individually.

var provider = ModShopApi.RegisterLayout(mod, "two-row-shop",
    shop => myLayoutEnabled ? new ModShopLayout(2) : null,
    priority: 10);

int rows = ModShopApi.GetLayout(shop).FabricRows;
bool visible = ModShopApi.IsFabricPreviewVisible(shop, bolt);
int capacity = ModShopApi.GetAccessoryHolderCapacity(holder);

// Dispose when this integration stops owning the layout.
provider.Dispose();

An optional visibility callback returns true or false for a fabric bolt, or null to use the viewport bounds. Visibility queries report whether a bolt is visible. Neighboring-texture preloading follows the configured preload range. Holder capacity reads the current slot array. Your mod arranges the objects; the provider describes that layout to Mod Loader.

Call these APIs on Unity's main thread. Keep provider callbacks independent of layout queries to avoid recursion. A callback that throws is logged and removed; the next provider or fallback layout is used. Disposing a registration or its owner takes effect immediately, including inside callbacks. These registrations are cleaned up with their owner.

Share game hooks

Keep a ModContext and dispose it when your plugin unloads. The following overloads track their registrations automatically. Existing overloads remain available.

// Higher priorities are considered first. The first suggestion wins.
AccessoryPlacementApi.Register(mod, "snap", context =>
    new AccessoryPlacementSuggestion { ModeLabel = "My guide" },
    includeTrims: true, priority: 10);

// Higher priorities apply later. Modify the view material, not the source fabric.
FabricRenderApi.Register(mod, "fabric-tint", fabric, FabricRenderSurface.Shop,
    context => context.Material.SetColor("_BaseColor", Color.white), priority: 10);

ModSettingsApi.Register(mod, "options", "My mod", ModSettingsCategory.Accessories,
    new ModToggleSetting("Show guide", "Show the placement guide.", showGuide));

// Observe refreshes without depending on one UI row for every owned item.
ModInventoryApi.Subscribe(mod, kind => RefreshMyOverlay(), priority: 0);
ModInventoryApi.Refresh(ModInventoryKind.Fabrics);

ModLifecycleApi.Subscribe(mod, ModLifecycleEvent.AfterSave,
    context => RefreshMySaveStatus(), priority: 0);

Placement rules and lifecycle/inventory notifications use higher priorities first. Fabric overrides use higher priorities later, allowing them to override earlier material changes. Ties between owned rules sort by plugin ID and then rule ID where supplied; callback subscriptions from the same plugin retain registration order. Legacy placement and fabric registrations retain their relative registration order.

A failing placement rule is disabled for the session. A failing inventory or lifecycle subscription is removed and its error is logged; other subscribers continue. Request inventory refreshes outside notification callbacks. Material callbacks keep the existing rollback behavior: a failed callback restores its material checkpoint. Use ModSaveData for plugin save data and ModInputApi to claim shared input instead of patching these mechanisms separately.

Check capabilities before using these overloads: owned-placement-rules, owned-fabric-overrides, owned-settings, and inventory-events are version 1; priority lifecycle subscriptions are lifecycle-events version 2. API calls and disposal belong on Unity's main thread. Use these registrations when you want your plugin to share the loader's hooks.

A C# plugin can use Mod Loader to look after its textures, keep data for each save, and share input with other mods. Register the plugin first. Mod Manager already lists BepInEx plugins; registration adds your author name and credits.

Control when your DLL loads

Other DLL plugins load after Mod Loader by default. A plugin that Mod Loader itself depends on still loads first. Players can open a DLL mod's options in Mod Manager to enable or disable it, change its load order, or read its loading details. These changes take effect after restarting the game.

For a required dependency in C#, use BepInEx's attribute:

[BepInDependency("yuu.dressmaker.mods", "0.7.2")]
[BepInDependency("example.other-mod", BepInDependency.DependencyFlags.SoftDependency)]

The first line requires Mod Loader 0.7.2 or later. The second waits for another plugin if it is installed, but allows your mod to load without it. Check that optional plugin is available before using its code.

Add a plugin metadata file

Put MyPlugin.plugin.json beside MyPlugin.dll. The filename must match. A plugin ZIP can include both files in the same folder. The file contains an array because one DLL can supply more than one plugin.

[
  {
    "id": "example.my-plugin",
    "loadAfter": ["example.fabric-tools"],
    "loadBefore": ["example.shop-preview"],
    "dependencies": [
      { "id": "yuu.dressmaker.mods", "minimumVersion": "0.7.2" },
      { "id": "example.fabric-tools", "minimumVersion": "1.0.0", "optional": true }
    ],
    "conflicts": [
      { "id": "example.older-replacement", "reason": "Both mods replace the same shop materials." }
    ]
  }
]

Use the plugin ID from BepInPlugin, not the DLL filename or display name. The ID must belong to a plugin in that DLL. Leave out fields you don't need.

  • loadAfter and loadBefore change ordering when the named plugin is installed. Declare required plugins in dependencies.
  • dependencies are required unless optional is true. A missing, disabled or older required dependency prevents your plugin from loading.
  • An optional dependency with an older version produces a notice. Your plugin still loads and must check the version before using that integration.
  • conflicts prevent your plugin from loading alongside the named plugin. Give a reason players can understand.

A rule that creates a dependency cycle is rejected and listed under Loading details. Native BepInEx dependencies are retained. Choose load order for initialization dependencies. Use Harmony priorities and ordering attributes to order patches on a shared method.

Player overrides are saved in BepInEx/config/DressmakerMods/plugin-rules.json. The startup report is in plugin-startup-report.json in the same folder. For exceptions thrown while a plugin starts, check BepInEx/LogOutput.log.

Wait for fabrics and other content

Wait for content registration before looking up fabrics, accessories, or garment pieces:

private void Awake()
{
    mod = ModLoaderApi.Register(this);
    ModLifecycleApi.WhenContentReady(mod, () =>
    {
        Logger.LogInfo("Content is registered. My plugin can look up items now.");
    });
}

The callback runs on Unity's main thread after startup content registration. If registration has already finished, it runs immediately. Deferred textures and models are still loaded when needed. The returned subscription belongs to your mod context and is removed when you dispose that context.

Register your plugin

Add a dependency on yuu.dressmaker.mods, then call ModLoaderApi.Register in Awake. Keep the returned ModContext in a field:

using BepInEx;
using DressmakerMods;

[BepInPlugin("example.tailor-notes", "Tailor Notes", "1.0.0")]
[BepInDependency("yuu.dressmaker.mods")]
public sealed class TailorNotes : BaseUnityPlugin
{
    private ModContext mod;

    private void Awake()
    {
        mod = ModLoaderApi.Register(this, new ModInfo {
            Author = "Your name",
            Description = "Keeps a few notes for each save."
        });
        ModLifecycleApi.Subscribe(mod, ModLifecycleEvent.SaveLoaded, OnSaveLoaded);
    }

    private void OnSaveLoaded(ModLifecycleContext context)
    {
        int visits = mod.SaveData.Read<int>("visits");
        mod.SaveData.Write("visits", visits + 1);
    }

    private void OnDestroy() => mod?.Dispose();
}

Use the C# build instructions for the project setup. This example also needs references to the game's Assembly-CSharp.dll and Newtonsoft.Json.dll.

The ID and version come from BepInPlugin. ModInfo.Name can change the display name; Author, Description and Credits are optional text. Register a plugin once. ModLoaderApi.Find(id) returns its context, or null; GetMods() returns a snapshot of registered plugins.

Call these APIs on Unity's main thread. Disposing the context removes its subscriptions, shader registrations and input claims, and releases textures loaded through it. The loader also checks for a destroyed plugin component. Keep OnDestroy anyway so cleanup happens as soon as your plugin unloads. Disabling a DLL in Mod Manager takes effect after restarting.

For other registrations which return IDisposable, use mod.Track(handle) to have the context dispose them too. File-based content definitions and items already used by a dress remain registered for the session.

Load a texture

Put your DLL and images in their own folder under BepInEx/plugins. Paths passed to mod.Textures.Load are relative to the DLL's folder.

ModTextureHandle cloth = mod.Textures.Load("images/cloth.png");
material.mainTexture = cloth.Texture;

The default is a streamed, sRGB texture with mipmaps, repeat wrapping and bilinear filtering. It starts as a small preview and shares the texture memory budget in Graphics. Materials on visible scene renderers are tracked automatically. For your own UI images, hold a pin while the image needs full resolution. Keep the handle alive while a material uses its texture. Dispose it when finished, or let the plugin context dispose it.

For a normal map or mask, choose linear colour space. Wrap settings survive streaming:

ModTextureHandle mask = mod.Textures.Load("images/mask.png", new ModTextureOptions {
    ColorSpace = TextureColorSpace.Linear,
    WrapU = UnityEngine.TextureWrapMode.Clamp,
    WrapV = UnityEngine.TextureWrapMode.Clamp
});
Option Default Use
ColorSpace SRgb Use Linear for data such as masks and normal maps.
Stream true Allow the loader to reduce resolution when the image isn't in use. Set false for an image which must stay at full size.
Mipmaps true Smaller copies for distant rendering. Turning this off also disables streaming for that texture.
WrapU, WrapV Repeat How the image behaves outside its UV range.
Filter Bilinear A Unity FilterMode, such as Point for pixel art.

Before an operation which needs the original image immediately, hold a pin:

using (cloth.Pin())
{
    // Render the full-resolution image here.
}

A pin restores full resolution synchronously and prevents eviction until disposed. It can temporarily exceed the memory budget. ModTextureApi exposes the current managed texture usage and budget.

Offer a shader in Fabric Creator

Load your shader from an AssetBundle, then register it in Awake. Pack fabrics which refer to a provider ID wait until plugin Awake calls have finished.

ModShaderRegistration shaderChoice = ModShaderApi.Register(mod, new ModShaderDefinition {
    Id = "woven",
    Name = "Woven cloth",
    Shader = wovenShader,
    MainTextureProperty = "_BaseMap"
});

wovenShader is the loaded Unity Shader. Replace _BaseMap with its main image property. The full ID is your plugin ID followed by the local ID, for example example.tailor-notes.woven. Fabric Creator shows the name in its shader selector and saves the full ID in material.shader.

Member Use
ShowInFabricCreator True by default. Set false to keep the shader available only to code and packs.
MainTextureProperty Optional texture property. When omitted, normal Unity main-texture handling is used. Registrations sharing a Unity shader must agree on this property.
ConfigureMaterial Optional Action<Material> for defaults. The fabric's JSON properties are applied afterwards.
ConfigureView Optional Action<FabricRenderContext> for changes in individual game views. See the Fabric render API.
ModShaderApi.Find(id) Find a registered shader choice, or return null.
ModShaderApi.GetShaders() Return a snapshot of the choices.

Keep the shader and its bundle assets loaded while fabrics use them. Existing fabric materials keep their shader when you remove its creator choice. Fabrics using a provider shader still need that plugin on the recipient's machine; dress export will ask for a portable definition if one hasn't been supplied.

Create an accessory when it is needed

Use AccessoryModApi.Register for an accessory described by ordinary pack fields. For a model built in C#, use RegisterFactory. Both take the plugin context and a ModAccessoryDefinition; file paths are relative to the plugin folder.

AccessoryModApi.WhenReady(() => {
    var baseItem = AccessoryModApi.Find("Brooch 01 Silver");
    AccessoryModApi.RegisterFactory(mod, new ModAccessoryDefinition {
        Id = "small-brooch",
        Name = "Small brooch",
        BaseAccessory = baseItem.name,
        AccessoryType = baseItem.accessoryType.name,
        Cost = 10,
        UnlockLevel = 0
    }, () => {
        var prefab = UnityEngine.Object.Instantiate(AccessoryPrefabApi.GetPrefab(baseItem));
        prefab.gameObject.SetActive(false);
        prefab.transform.localScale *= 0.8f;
        return prefab;
    });
});

The definition appears in the catalogue immediately. The factory runs on Unity's main thread the first time the game needs its prefab; later requests use the same object. Keep factory work short to avoid a visible pause. See AccessoryPrefabApi for state checks and loading callbacks.

Return your own inactive clone. The loader destroys that clone when the registration is disposed and restores the base prefab for future uses. Existing placed copies remain. The factory still owns any separate meshes, materials or textures it creates, so keep those alive until they are no longer used. If the factory throws, the game uses the base accessory and the error appears in Loading problems.

Keep data for each save

mod.SaveData stores JSON values under your plugin's ID. Use a short local key such as visits or quest-progress. Another plugin using the same local key gets a separate entry.

int count = mod.SaveData.Read<int>("visits");
mod.SaveData.Write("visits", count + 1);
mod.SaveData.Delete("old-setting");

Read and write after a save has loaded, usually from SaveLoaded or a gameplay action. A missing entry returns default(T). Writes and deletes first change the in-memory data. They are written to player.json.moddata.json, player1.json.moddata.json, and so on when the game writes that slot's save file. The next successful game save commits those changes.

The sidecar keeps entries for absent plugins. Save backups include it, its previous copy and preserved damaged copies. Deleting or resetting a slot archives its old sidecar. If the main sidecar is damaged, the loader tries its .bak file; if neither can be read, it reports the problem and leaves the files alone. A newer format is kept unchanged too.

When restoring a save manually, restore its matching sidecar as well. When a recovery file has no matching plugin data, the loader preserves the existing sidecar. ModSaveDataApi.CopySlotData(sourceSlot, destinationSlot) copies stored sidecar data after your code copies the game save. Use two inactive slots for this operation.

Change your data format

Give an entry a schema version when its shape changes. A migration receives the old data and version, and returns the new JSON value:

using Newtonsoft.Json.Linq;

int visits = mod.SaveData.Read<int>("visits", schemaVersion: 2,
    migrate: (oldData, oldVersion) => {
        if (oldVersion == 1)
            return new JValue((int)oldData["count"]);
        throw new System.IO.InvalidDataException("Unknown visits format: " + oldVersion);
    });
mod.SaveData.Write("visits", visits + 1, schemaVersion: 2);

This migration assumes version 1 stored an object with a count field. Adjust it to your actual old format. If the migration throws an exception, the old entry is left untouched. Reading or overwriting an entry with a newer version is rejected. Keep each entry under 1 MiB; the whole sidecar has an 8 MiB limit.

Keep content IDs and fallbacks

Keep local IDs stable when changing a display name. If an ID really must change, register an alias after registering the replacement item:

ModContentApi.RegisterAlias(mod, "old-cloth-id", newFabric);
ModContentApi.RegisterFallback(mod, newFabric, Fabric.CottonBlack);

The old local ID must belong to your plugin's namespace and be unused. Old saves resolve it to the new item; subsequent saves use the new ID. Aliases are removed when the context is disposed.

A fallback must be an original game item of the same type. The loader records it beside the save, so it can still use it when your plugin is absent. Missing items keep their custom IDs; reinstalling the mod restores them. Garment fallbacks must match the kind of piece, such as bodice to bodice. These recovery rules need Mod Loader to be installed.

ModContentApi.Describe(item) returns the recorded owner ID, full name, GUID, content kind and RequiresPlugin flag, or null for an item without a content record.

Allow an item in dress exports

Ordinary file-based definitions already have export records. A factory or runtime material needs a separate definition which another player can load without running your plugin:

ModContentApi.SetExportDefinition(mod, newFabric, new FabricDefinition {
    Id = "new-cloth-id",
    Name = "Printed cotton",
    BaseFabric = Fabric.CottonBlack.name,
    Texture = "images/printed-cotton.png"
});
ModContentApi.SetExportAttributions(mod, newFabric, new[] {
    new ModAttribution { Name = "Printed cotton", Author = "Your name", License = "Your license" }
});

The local ID must match the item. There are equivalent overloads for AccessoryDefinition with ModAccessoryDefinition, and GarmentComponent with ModGarmentDefinition. Supply real files and the same required fields as a content pack. Test an exported dress in a separate installation without the original plugin. Use file-based assets for portable definitions. Content that depends on callbacks, runtime textures, or provider shaders needs its plugin installed.

Set export credits after the definition. They use the same attribution fields as a pack. Plugin name, author and credits are used by default. License files beside the DLL or in its licenses folder are included by the normal dress bundler.

Reserve input while a tool is active

Register a claim once, with a function that says when your tool needs the input. The loader checks that function before gameplay input is read, including before your plugin's Update.

ModInputApi.Register(mod, "resize-wheel", ModInputChannel.MouseWheel,
    () => resizingPoint && UnityEngine.Input.GetKey(UnityEngine.KeyCode.LeftShift));

// In Update, while your point editor is active:
float steps = ModInputApi.ReadMouseWheel(mod).y;

resizingPoint is a boolean field maintained by your editor. Only read the wheel for resizing while that field is true. ReadMouseWheel also returns zero over UI, while typing or when another plugin owns the wheel. An inactive claim leaves normal game controls alone.

Channel Reserved gameplay input
MouseWheel The game's InputReader.MouseWheelDelta, including mannequin scroll rotation.
PrimaryPointer Mod Loader's select/drag action and the game mouse reads routed through it.
SecondaryPointer The corresponding right-click action.
Camera Mapped camera movement and zoom, mannequin dragging and the game's wheel reader.
Keyboard Mapped game and mod keybinds. The owning plugin's namespaced keybinds remain available.

Combine channels with | when registering. Query them one at a time with GetOwner(channel) or HasControl(mod, channel). With no active claim, GetOwner returns null and every plugin has control. Higher priority wins; ties use plugin ID, then local claim ID in ordinal order. The default priority is zero.

Use ModKeybindApi for remappable shortcuts. Check ownership before reading UnityEngine.Input directly so your plugin respects another tool's active claim. Unity's UI controls remain usable. If the function throws an exception, the loader disables that claim and logs the error once.

Listen for game events

ModLifecycleApi.Subscribe(mod, event, callback) returns a disposable subscription. The context also owns it. Each callback receives Event, SaveSlot and SceneName.

Event When it runs
ContentReady After the startup content pass, including pending shader fabrics. A late subscription runs immediately. Deferred models and textures load when requested.
SaveLoading After selecting sidecar data, before the game reads the save. The game's current progress may still be from the previous slot; use SaveLoaded for progress changes.
SaveLoaded After the game returns a loaded progress object, including a fresh slot.
BeforeSave Before a save attempt. Write any pending plugin state here.
AfterSave After the save call returns and its main file was written. A sidecar write error is reported separately in Loading problems.
SceneLoaded From Unity's scene-loaded callback. Objects have run Awake, but their Start methods may not have run.
ReturnedToTitle When TitleScreen loads, including initial startup if already subscribed.
SaveDeleted After slot deletion. SaveSlot identifies the deleted slot.

A throwing subscriber is removed and its error is logged; remaining subscribers continue. Recursive raises of the same event are ignored.

Report a loading problem

mod.Diagnostics.Report(new ModProblem {
    Code = "missing-mask",
    Message = "Lace cuffs need images/cuffs-mask.png. Reinstall the full mod ZIP.",
    ItemId = "lace-cuffs",
    File = "images/cuffs-mask.png",
    Severity = ModProblemSeverity.Error,
    Details = error.ToString()
});

error is the exception from your catch block. The report appears in Mod Manager's Loading problems and the loader log with your plugin ID. Code and Message are required; use Warning when the plugin can continue. For a short report, call mod.Diagnostics.Warning(code, message) or Error(code, message).

Check for a feature

For a complete startup example and readiness checks, see Start and stop a plugin. For custom previews, see Track textures used by your plugin.

bool hasStreaming = ModLoaderApi.Supports("managed-textures", minimumVersion: 1);

The following capability IDs have contract version 1: plugin-registration, diagnostics, managed-textures, shader-registration, accessory-factories, save-data, content-recovery, portable-content, input-claims, friendship-rewards, owned-placement-rules, owned-fabric-overrides, owned-settings, inventory-events and shop-layout. The lifecycle-events contract is version 2 and retains its version 1 overloads. An unknown ID, unsupported minimum version, or failed required feature returns false. Checks return false before startup finishes. The save-data capability also waits for deferred content to finish loading. Check capabilities again before using optional support; a completed startup can include skipped features.

Use capability checks for Mod Loader API support, ModLoaderApi.Find for registered plugins, and ModShaderApi.Find for shader choices. Set your BepInEx dependency to a loader version that provides the APIs your plugin references.

Back to the Mod Loader guide