Lifecycle
Audience: Plugin authors · Prereqs: Anatomy
Canonical source:
src/shell/plugins.js(PluginManager.loadPlugins,.preloadPlugin)
What actually happens, in order, between a server process starting and your plugin's constructor running.
At a glance
The boot sequence
Every server role (services, game, client) follows the same pattern in its run-*.js entrypoint:
misc.instantiateSS(...)- builds the sharedsscontext object (config, paths, version info).plugins.loadPlugins('<services|game|client>')- all plugins load here, before anything else.- The role's own
start-*.jsis dynamically imported and runs.
Step 2 happening before step 3 is deliberate: it means a plugin can monkey-patch or register against shared modules before the server's own core logic runs against them. It's also why plugins.type is already set correctly by the time your plugin's constructor executes.
What loadPlugins(type) actually does
Per-plugin, in order:
- Scans both
plugins_default/andplugins/for subdirectories not starting with_. - For each plugin folder, in parallel:
- Reads
dependencies.jsif present. - If the folder is a git repository, runs
git pullin the background (non-blocking, logged but non-fatal if it fails - see below). - Resolves/installs npm dependencies, and checks any
"plugin"-type dependencies are present (see Dependencies). A missing plugin dependency drops this plugin from loading entirely. - Imports
index.js.
- Reads
- Sorts all successfully-preloaded plugins alphabetically by
PluginMeta.identifier. - Instantiates them in that sorted order, one at a time:
new Plugin(pluginManagerInstance, pluginFolderPath).
Step 2 (preloading - reading files, git-pulling, resolving dependencies) happens in parallel across all plugins for speed. Step 4 (actually running each constructor) happens strictly in the sorted order, one after another, awaited sequentially - so a slow or blocking constructor in one plugin delays every plugin after it alphabetically, but never one before it.
Load order and why some plugins have number-prefixed identifiers
Since instantiation order follows PluginMeta.identifier alphabetically, and some plugins need to run their constructor before or after another specific plugin (e.g. to make sure a dependency's setup has actually happened, beyond just "the folder exists" which is all dependencies.js checks), you'll see identifiers deliberately prefixed with a digit purely to force sort order - 5_crackshot is a real example in this codebase, chosen specifically to control its position relative to other weapon-model plugins. This is a workaround, not a formal API - there's no other way to express "load after plugin X" today.
Git auto-pull on every load
If a plugin's folder is its own git repository, LegacyShell runs git pull on it every time that server boots - a lightweight built-in auto-update mechanism, no separate deploy step needed for plugin updates. A few things worth knowing:
- It runs asynchronously and non-blocking - the plugin loads with whatever code was already on disk; the pull result only affects the next boot.
- Failure is logged, not fatal - a plugin without internet access, or one that's just a plain folder (not a git repo), loads normally; you'll only see a warning in the log.
- This means every server restart is a potential update point for every plugin with a git remote - worth knowing if you're debugging "my change to a plugin didn't take effect," since an auto-pull could have silently reverted or advanced what's on disk if the plugin folder's remote changed underneath you.
Server-type gating
There's no dedicated API for "only load on the game server" - plugins.type ('services', 'game', or 'client', set by whichever loadPlugins(type) call is currently running) is just a plain property, checked by convention at the top of the constructor:
export class Plugin {
constructor(plugins, thisDir) {
this.plugins = plugins;
this.thisDir = thisDir;
if (plugins.type !== "game") {
log.orange(`${PluginMeta.identifier} won't run on this server type.`);
return;
};
// ...actual setup, only reached on the game server
};
};
Every server type still instantiates your Plugin class (there's no way to skip that) - the convention is just to return early from the constructor once you've confirmed you're not on a relevant server type, so nothing else in your plugin runs.
One important nuance: room workers load plugins independently
If your plugin targets game: events, be aware that plugins.loadPlugins('game') doesn't run just once per game server process - it runs again, completely independently, inside every room's worker thread (see Workers and State for the full explanation and why it matters for anything involving shared state). Nothing about the boot sequence described above changes because of this - it's just that "the game server" is, in practice, several separate JS execution contexts each running this exact sequence on their own.
Next: Dependencies.
This page was drafted with AI assistance and reviewed for accuracy. If something looks wrong, please open a PR or flag it.
