Skip to content

Plugin lifecycle

plugins/Homes/Homes.dll (+ plugin.json beside it, if any)
│ 1. plugin.json is read first: id, name, version, main, depends, ...
│ 2. the assembly is loaded from bytes into a new collectible AssemblyLoadContext (the file is never locked)
│ 3. the plugin class is found: "main" from plugin.json, else the first public IPlugin with a parameterless constructor
│ 4. a PluginContext is created (name and version from plugin.json or [PluginInfo], else the assembly)
▼
OnEnable() register events, commands, systems, components, queries, tasks
│ ... the plugin runs ...
▼
OnDisable() optional cleanup of your own state
│ 5. everything registered through the context is released, in reverse order
│ 6. native leftovers are dropped by owner tag (events, commands, systems, queries, components)
│ 7. the load context is unloaded and garbage collected

Plugins run in a separate, sandboxed worker process, not inside the server. The worker loads every plugin from the plugins folder, and all of the steps above run on the worker’s tick thread, in step with the server’s ticks. Plugin code never runs on the server’s own threads.

The worker is started with the server and restarted when it needs to be (see hot reload). On shutdown and on /plugins reload, plugins are disabled first (OnDisable runs, so they can save) and then the worker exits. After a crash, the server starts a new worker and loads every plugin again from disk, so state that is not in files or components is lost.

PluginMain.cs
[PluginInfo("Homes", Version = "1.0.0", Author = "you")]
public sealed class PluginMain : Plugin
{
protected override void OnEnable()
{
Log.Info("homes enabled");
}
protected override void OnDisable()
{
Log.Info("homes disabled");
}
}

Plugin exposes the plugin-scoped services as properties: Log, Events, Ecs, Systems, Commands, Scheduler, Permissions, Config, DataFolder, Services, Info, Channels and the raw Context (a PluginContext). Everything you register through them is tracked by the context, so you never unregister by hand; unloading does it.

  • Do no work in the constructor. Use OnEnable.
  • You can implement IPlugin directly if you do not want the base class: Enable(PluginContext) receives the context, and Disable() is called on unload.
  • Name and version come from plugin.json when there is one. Without a manifest, [PluginInfo] names the plugin. Without either, the plugin is named after its assembly, with the assembly’s version.

A plugin.json next to the dll is read before the assembly is loaded. It declares what the loader needs to decide whether and in what order to load the plugin. The fields are described in the plugin.json reference; the ones that matter for the lifecycle:

{
"id": "homes",
"name": "Homes",
"version": "1.0.0",
"main": "PluginMain",
"depends": ["economy >=1.2"],
"softDepends": ["placeholders"],
"loadBefore": []
}
  • depends: must be loaded first. A missing one, or one with a version that does not satisfy the constraint, keeps this plugin from loading.
  • softDepends: loaded first when present. The plugin must cope with their absence (check Plugins.IsLoaded or a service).
  • loadBefore: this plugin is loaded before the listed ones.
  • api: the plugin API version the plugin was written for. A server that does not provide it rejects the plugin.

Plugin.Info gives you the same data at runtime.

The loader sorts the plugins with a topological sort of depends, softDepends and loadBefore, and breaks ties by id, so the order is deterministic. Unloading runs in reverse order: a plugin’s dependents are unloaded before it, because they hold its types.

Plugins is a read-only view of what is loaded: Plugins.All, Plugins.Get(id), Plugins.IsLoaded(id) and Plugins.Problems (one sentence per plugin that did not load, and why). Use PluginEnableEvent and PluginDisableEvent to react when another plugin comes or goes; both fire after the plugin’s registrations are set up or released.

Situation Result
No public IPlugin with a parameterless constructor (or the main class is not one) no IPlugin implementation with a parameterless constructor is logged, nothing is loaded
A dependency is missing, too old, or part of a cycle, or the api version is not supported the plugin is not loaded, with a message such as plugin 'homes' requires 'economy >=1.2' which is not installed; plugins that depend on it are not loaded either
OnEnable throws the exception is logged, every registration made so far is released, the plugin is not loaded, and Plugins.Problems names the cause; other plugins are unaffected
OnDisable throws logged (error in OnDisable), unloading continues
A handler, task or system throws caught at the boundary and logged with the plugin name; the server keeps running and the next call goes through

Everything you create through the plugin’s services belongs to the plugin and goes away with it:

Registered through Released on unload
Events.On the subscription
Commands.Register the command
Systems.Add/Register/After/Before/Replace the system (a Replaced system is restored)
Ecs.Query<...>() the query
Ecs.RegisterComponent<T> the type binding (values on entities survive a reload, see hot reload)
Scheduler.RunLater/RunEvery the pending scheduled task; queued Post, RunSync and ToTick work of the plugin is dropped
BossBars, Holograms, Npcs, Sidebars, ChunkTickets, Menus, Channels, Permissions the plugin’s boss bars, holograms, NPCs, sidebars, chunk tickets, menus, channel listeners and permission nodes
Context.Track(resource) anything implementing IPluginResource

Entities are not owned: they live in the world, not in the plugin. Destroy the ones you created in OnDisable if they should not outlive it.

PluginEnableEvent and PluginDisableEvent are fired when a plugin enables or disables, including on hot reloads. A disable event fires after the plugin’s registrations are already released.