Plugin lifecycle
From file to running plugin
Section titled “From file to running plugin”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 collectedPlugins 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.
The plugin class
Section titled “The plugin class”[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
IPlugindirectly if you do not want the base class:Enable(PluginContext)receives the context, andDisable()is called on unload. - Name and version come from
plugin.jsonwhen there is one. Without a manifest,[PluginInfo]names the plugin. Without either, the plugin is named after its assembly, with the assembly’s version.
The manifest
Section titled “The manifest”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 (checkPlugins.IsLoadedor 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.
Load order
Section titled “Load order”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.
What happens on errors
Section titled “What happens on errors”| 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 |
What the context owns
Section titled “What the context owns”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.
Enable and disable events
Section titled “Enable and disable events”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.