Skip to main content
This page describes how the Mudstack desktop app discovers, loads, and communicates with plugins.

Overview

  • Plugins are discovered from configured directories; each plugin is a folder with a manifest and an entry point.
  • Each plugin runs in a separate Node.js child process; the main app and plugins communicate over a message bus (IPC).
  • The runtime resolves dependencies (other plugins or declared executables), then loads and activates plugins in dependency order.
  • Events and requests flow between the main process and plugin processes (and between plugins) via the message bus.

Discovery

  1. Plugin directories – The app is configured with one or more directories to scan (e.g. user plugins folder Documents/Mudstack/plugins, built-in plugins under app resources).
  2. Per-directory scan – Each subdirectory of a plugin directory is treated as a candidate plugin.
  3. Manifest – A candidate must contain manifest.json or plugin.json with at least:
    • id – Unique string (e.g. com.example.my-plugin).
    • name – Display name.
    • main – Entry file (e.g. index.js) relative to the plugin folder.
  4. Validation – The manifest is validated (required fields, dependency structure). The main file must exist on disk.
  5. Registration – Valid plugins are registered in an internal registry with metadata (id, manifest, path, source type). Enable/disable state is stored separately (e.g. in app config) and checked when loading.
  6. Refresh – The app can refresh the plugin list (e.g. after adding a new folder); new plugins are discovered and can be loaded if enabled.
Plugins are not loaded or activated during discovery; that happens later when the app loads enabled plugins.

Process isolation

  • Each enabled plugin is run in its own Node.js child process (fork of a small loader script).
  • Environment variables (e.g. PLUGIN_ID, PLUGIN_PATH) tell the child which plugin to load.
  • The child loads the plugin’s main module, instantiates the exported class with the Plugin API, then waits for an activate command from the main process.
  • All communication between main and plugin is over the message bus (request/response and fire-and-forget). The Plugin API in the child sends messages to the main process; the main process routes events, DB calls, config, and responses back to the correct plugin.
Benefits:
  • A crash or infinite loop in one plugin does not bring down the main app.
  • Heavy or blocking work in a plugin does not freeze the UI.
  • Plugins can be restarted (e.g. after failure or reload) without restarting the app.

Message bus and Plugin API

  • Main process holds a PluginManager and a MessageBus. For each plugin, it keeps a reference to that plugin’s child process and registers it with the message bus.
  • Plugin process holds a MessageBus client and a PluginAPI instance. The API turns high-level calls (e.g. api.log(), api.events.on(), api.db.assetAPI.getAsset()) into messages sent to the main process.
  • Main process handles those messages (e.g. writes logs, dispatches events, calls the real DB or config layer) and may send responses or event deliveries back to the plugin.
  • Events – When the app or a plugin emits an event, the main process event system delivers it to all subscribers. Subscriptions registered by plugins (via api.events.on() or via manifest subscriptions) are tracked; when an event matches, the main process sends an event-delivery message to the subscribing plugin’s process, and the plugin’s EventManager invokes the handler.
So: your plugin code only sees this.api (logging, events, db, config, execute, requestFromPlugin, etc.). The complexity of IPC and routing is hidden behind that API.

Dependency resolution

  • A plugin’s manifest can declare dependencies (e.g. other plugins or external executables).
  • For plugin dependencies: the runtime ensures the dependency is registered and, if it’s enabled, loaded and activated before the dependent plugin. Load order is topological (dependencies first).
  • For optional dependencies, the plugin can still load if the dependency is missing; you can use api.hasDependency() in code to adapt.
  • Non-plugin dependencies (e.g. executables, libraries) can be validated (e.g. by path or env) so the plugin only activates when the tool is available; see the manifest dependency types and the app’s dependency resolution logic.

Context menu and subscriptions (manifest)

  • contextMenuCommands in the manifest declare menu items. When the user runs a command, the app emits the event type you specified (eventType). No plugin code runs in the main process; the event is emitted and delivered to subscribers like any other event.
  • subscriptions in the manifest list event + handler (method name on your class). The runtime wires these so that when that event is emitted, your plugin process receives it and the named method is invoked with the message (including request id and payload). You do not need to call api.events.on() for these; the runtime subscribes on your behalf.
  • registrations in the manifest are used to emit events (or register capabilities) when your plugin activates—e.g. to register as a thumbnailer for certain file types. The runtime sends these to the main process so the app can update its registries (e.g. which thumbnailer handles .fbx).

Summary

  • Discovery: Scan plugin directories → validate manifest and main file → register plugin.
  • Isolation: One Node.js child process per enabled plugin; communication via message bus.
  • API: Your plugin uses this.api; all operations are implemented via IPC under the hood.
  • Dependencies: Resolved and loaded in order; optional dependencies allow graceful degradation.
  • Context menu and subscriptions: Declared in the manifest; the runtime wires events to your handler methods.
Next: Plugin lifecycle (states, activate, deactivate) and Plugin API (detailed API reference).