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.
Load order
Section titled “Load order”- Scan folders. For each plugin folder, the editor looks at its direct subfolders in name order. Only folders that contain
plugin.jsonare plugins. - Check plugin.json. If a field is invalid, the state is Error (file).
- Compare SDK versions. If the editor’s SDK version is outside the
sdkrange, the state is Version mismatch. - Check enabled. A plugin the user disabled is Off.
- 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).
- 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
.pdbis in the same folder, it is loaded too. - Create the entry class. The editor creates
entry(or the firstINpEditorPluginimplementation if empty) with its parameterless constructor. If the class cannot be found or created, the state is Error (isolated). - Call
Register(host). If it throws, nothing is registered and the state is Error (isolated). - 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.
- 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.
States
Section titled “States”These appear in the Plugin Manager Status column and in the console plugins output.
| State | Meaning | What to do |
|---|---|---|
| Pending | Found but not loaded yet (for example, before the warning is confirmed) | Confirm the warning, or click Rescan |
| On | Running normally | — |
| Off | The user cleared the Enable checkbox | Check it again |
| Error (file) | plugin.json is invalid · dll missing · duplicate id | Fix the reason shown on the detail line below |
| Version mismatch | The sdk range does not include the editor SDK, or loading failed because the plugin uses a member the editor SDK lacks | Fix 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 unloaded | Fix the exception from the Output Log and click Reload (selected) |
Plugin Manager
Section titled “Plugin Manager”Open it with Tools › Plugin Manager….
| Column · button | What it does |
|---|---|
| Enable | Check to enable (load); clear to unload. The state is stored in plugins_state.json. |
| Name · Version · Status | The name and version from plugin.json, and the current state. |
| Registered | If enabled, the registration summary (for example Menu 1 · Quick add 1); otherwise the error reason. |
| Rescan | Scans 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 folder | Opens the user plugin folder. |
Disable · reload · unload
Section titled “Disable · reload · unload”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.
- 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.
- If the entry class is
IDisposable,Dispose()is called. - World logic types are removed from the registry. Rules that use those types are not deleted; they remain with an Unknown type warning.
- If the plugin was holding the play camera, it is released (back to the original view).
- Everything drawn with
host.Drawis cleared. - 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.
Dispose rules
Section titled “Dispose rules”| What to do | Why |
|---|---|
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}Execution thread
Section titled “Execution thread”- All callbacks (menus · commands · events ·
NpUi·Simulate·PlayTicked) are called on the editor main thread. - Move long computations off with
Task.Run. - Call
World.EditandDrawonly on the main thread (inside a callback).
File locations
Section titled “File locations”| What | Location |
|---|---|
| User plugin folder | %APPDATA%\NPEditor\plugins\<folder>\ (Tools › Open plugin folder) |
| Install folder plugins | <install folder>\plugins\<folder>\ |
| Additional folders | Environment 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 data | Your 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.
id namespace
Section titled “id namespace”| What you register | Name inside the editor | On collision |
|---|---|---|
| Menus · quick add · import/export · panels · modes · shortcuts | ext.<plugin id>.<your id> | Never collides with other plugins or built-ins |
Shortcut Mode | If you use your mode id as-is, the editor adds ext.<plugin id>. | — |
| Console command names | As-is (no spaces) | If it already exists, only that command is not attached (the existing one wins) |
| World logic types | As-is (within the same role) | If it already exists (including built-ins), registration throws → the plugin is isolated |
Actor type Kind | As-is | If 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).
What isolation cannot prevent
Section titled “What isolation cannot prevent”- 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.