Skip to content

Loading and lifecycle

The editor scans the plugin folders for plugin.json. It loads only plugins that pass the checks and are enabled. If plugin code throws an exception, only that plugin is disabled and the editor keeps running. This is called isolation.

  1. Scan folders. For each plugin folder, the editor looks at its direct subfolders in name order. Only folders that contain plugin.json are plugins.
  2. Check plugin.json. If a field is invalid, the state is Error (file).
  3. Compare SDK versions. If the editor’s SDK version is outside the sdk range, the state is Version mismatch.
  4. Check enabled. A plugin the user disabled is Off.
  5. First-run warning. When a plugin is found for the first time, a warning dialog appears once. Click Trusted — load to remember the choice and load it. Click Don’t load this time to load nothing this time (the plugin stays Pending).
  6. Create the load context. One is created per plugin. The dll is read and loaded as bytes, so the file is not locked. If a .pdb is in the same folder, it is loaded too.
  7. Create the entry class. The editor creates entry (or the first INpEditorPlugin implementation if empty) with its parameterless constructor. If the class cannot be found or created, the state is Error (isolated).
  8. Call Register(host). If it throws, nothing is registered and the state is Error (isolated).
  9. Attach to the editor. Menus, modes, panels, commands and so on are attached to the editor. If an exception occurs while attaching, the state is Error (isolated). A command or actor type whose name already exists is the only item not attached.
  10. On. The Output Log records [Plugin <id>] <version> enabled — <registration summary>.

The editor runs steps 1–10 once at startup. Rescan in the Plugin Manager or the console command plugins reload runs steps 1–10 again. Plugins that are already on are not reloaded.

These appear in the Plugin Manager Status column and in the console plugins output.

StateMeaningWhat to do
PendingFound but not loaded yet (for example, before the warning is confirmed)Confirm the warning, or click Rescan
OnRunning normally—
OffThe user cleared the Enable checkboxCheck it again
Error (file)plugin.json is invalid · dll missing · duplicate idFix the reason shown on the detail line below
Version mismatchThe sdk range does not include the editor SDK, or loading failed because the plugin uses a member the editor SDK lacksFix sdk, or update the editor
Error (isolated)The entry class could not be created · an exception in Register or a callback → only that plugin is unloadedFix the exception from the Output Log and click Reload (selected)

Open it with Tools › Plugin Manager….

Column · buttonWhat it does
EnableCheck to enable (load); clear to unload. The state is stored in plugins_state.json.
Name · Version · StatusThe name and version from plugin.json, and the current state.
RegisteredIf enabled, the registration summary (for example Menu 1 · Quick add 1); otherwise the error reason.
RescanScans the folders again. Finds new plugins and unloads folders that disappeared.
Reload (selected)Unloads the selected plugin, rereads plugin.json and the dll, and enables it. Use this after building.
Open folderOpens the user plugin folder.

A plugin is unloaded when: its Enable checkbox is cleared · Reload (selected) · Rescan after its folder is deleted · error isolation. When the editor exits, the process ends without the unload sequence. So do not put must-run work such as saving in Dispose.

  1. The editor detaches everything that was registered (menus · modes · panels · commands · shortcuts · actor types · cell guards · symmetry). If one of the plugin’s modes was active, the editor returns to Block brush mode.
  2. If the entry class is IDisposable, Dispose() is called.
  3. World logic types are removed from the registry. Rules that use those types are not deleted; they remain with an Unknown type warning.
  4. If the plugin was holding the play camera, it is released (back to the original view).
  5. Everything drawn with host.Draw is cleared.
  6. The load context is unloaded (collectible unload).

host.Settings values remain in the file. They are read again as-is when the plugin is enabled again.

What to doWhy
Unhook anything attached to static events, timers or threads in Dispose.If anything remains, the load context cannot be unloaded from memory.
Stop work started with Task.Run in Dispose (CancellationToken).The plugin must not touch the world after it is disabled.
You do not need to unhook host events such as host.WorldChanged, Committed and View.PlayTicked.The host discards all of them on unload.
Do not throw exceptions from Dispose.They are logged to the Output Log and the unload continues.
public sealed class Plugin : INpEditorPlugin, IDisposable
{
private readonly CancellationTokenSource _stop = new();
public void Register(INpEditorHost host)
{
// … registration …
}
public void Dispose() => _stop.Cancel(); // stop background work
}
  • All callbacks (menus · commands · events · NpUi · Simulate · PlayTicked) are called on the editor main thread.
  • Move long computations off with Task.Run.
  • Call World.Edit and Draw only on the main thread (inside a callback).
WhatLocation
User plugin folder%APPDATA%\NPEditor\plugins\<folder>\ (Tools › Open plugin folder)
Install folder plugins<install folder>\plugins\<folder>\
Additional foldersEnvironment variable NP_EDITOR_PLUGINS (separate multiple with ;)
Enable · disable · warning confirmation%APPDATA%\NPEditor\plugins_state.json
Plugin settings (host.Settings)%APPDATA%\NPEditor\plugin_settings\<id>.json
Shortcuts changed by the user%APPDATA%\NPEditor\shortcuts.json
Your own large dataYour own files under host.PluginDir (= the folder containing plugin.json)

Folders are scanned in the order of the table above. For duplicate ids, the one found first is used.

What you registerName inside the editorOn collision
Menus · quick add · import/export · panels · modes · shortcutsext.<plugin id>.<your id>Never collides with other plugins or built-ins
Shortcut ModeIf you use your mode id as-is, the editor adds ext.<plugin id>.—
Console command namesAs-is (no spaces)If it already exists, only that command is not attached (the existing one wins)
World logic typesAs-is (within the same role)If it already exists (including built-ins), registration throws → the plugin is isolated
Actor type KindAs-isIf it already exists, only that type is not attached (the existing one wins)

Prefix command names, world logic types and actor kinds with your own name (for example road_build, myquest_start, myroad.zone).

  • Infinite loops · stack overflows · process exit (Environment.Exit)
  • File and network access

A plugin is code that runs with the same permissions as the editor. Isolation is a safeguard against mistakes, not a security boundary.