Plugin Lifecycle
Every RocketMod plugin follows a predictable lifecycle from the moment RocketMod discovers its assembly to the moment the plugin is removed from memory. Understanding this lifecycle is essential for writing plugins that initialize correctly, clean up after themselves, and survive reload cycles without leaking state or double-subscribing to events.
This article covers the complete lifecycle of a RocketMod plugin — from assembly discovery through load, active running, and unload — including the lifecycle events that fire at each transition. The 57 Studios™ team has debugged enough reload-corrupted plugin states to document not just the order of operations but the invariants that hold at each step.

Prerequisites
- A working RocketMod plugin project. See Creating Your First RocketMod Plugin.
- Familiarity with the
RocketPlugin<TConfig>base class andLoad()/Unload()methods. - Access to a test server for reload testing.
The lifecycle state machine
RocketMod manages plugins through a state machine with five states:
Discovered → Loading → Loaded → Active → Unloading → UnloadedThe Reload operation cycles through Unloading → Unloaded → Loading → Loaded on the same plugin instance.
Each state corresponds to a method call or event firing. The plugin code runs inside these transitions and must not assume a steady state outside of Active.
State descriptions
| State | Meaning | What the plugin can do |
|---|---|---|
Discovered | RocketMod found the assembly in Rocket/Plugins/ | Nothing — the class has not been instantiated yet |
Loading | The constructor and Load() are executing | Initialize state, subscribe to events, register commands |
Loaded | Load() completed successfully | Plugin is running normally; event handlers fire, commands work |
Unloading | Unload() is executing | Deregister events, flush state, release resources |
Unloaded | Unload() completed; plugin is inert | Nothing — the instance may be garbage collected |
Constructor phase
Before Load() runs, RocketMod instantiates the plugin class using its parameterless constructor. This happens after assembly discovery and before any lifecycle method is called.
csharp
public class MyLifecyclePlugin : RocketPlugin<MyLifecyclePluginConfiguration>
{
public MyLifecyclePlugin()
{
Rocket.Core.Logging.Logger.Log("[MyLifecyclePlugin] Constructor called.");
}
}What the constructor should do
- Store constructor parameters only. Since
RocketPlugin<TConfig>has no constructor parameters, there is nothing to store. - Initialize readonly fields that do not depend on configuration or runtime state.
- Do NOT subscribe to events in the constructor. Events fired during construction arrive before the plugin is fully initialized, and the subscription pattern in
Load()handles the lifecycle correctly.
What the constructor should NOT do
- Do NOT access
Configuration.Instance. The configuration file has not been deserialized yet. Accessing configuration in the constructor reads a null reference. - Do NOT call
R.Permissions,R.Commands, orR.Plugins. RocketMod's own services may not be fully initialized when the constructor runs. - Do NOT start timers, open file handles, or allocate scarce resources. The plugin may be constructed but never loaded if an earlier plugin fails to initialize.
Load phase
Load() is called after the plugin is constructed and its configuration is deserialized. This is where the plugin sets up its runtime state.
csharp
protected override void Load()
{
Rocket.Core.Logging.Logger.Log("[MyLifecyclePlugin] Load() called.");
Instance = this;
// Subscribe to events
UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected += OnPlayerDisconnected;
// Read configuration
int maxPlayers = Configuration.Instance.MaxPlayers;
Rocket.Core.Logging.Logger.Log(
$"[MyLifecyclePlugin] Loaded. Max players: {maxPlayers}");
}Order of operations in Load
- The configuration file is deserialized into
Configuration.Instance. If the file does not exist,LoadDefaults()is called and the defaults are written to disk. Load()executes on the plugin instance.- During
Load(), the plugin subscribes to events, sets the staticInstancesingleton, initializes caches or data structures, and logs the successful load. OnPluginLoadingfires during theLoad()method, after the plugin's own initialization code but beforeLoad()returns. This event is part ofRocket.Core.Plugins.Eventsand allows other systems to react to the plugin loading. The event receives the plugin instance and can inspect its state, though at this point the plugin's event subscriptions may not all be registered yet.
What to put in Load
- Event subscriptions: subscribe to all
UnturnedPlayerEvents,UnturnedServer, and custom events here. Match every+=with a-=inUnload(). - Singleton assignment: set
Instance = thisso commands and other classes can access the plugin. - Cache initialization: pre-load lookup tables, translation files, and frequently-used configuration values into memory for fast access during command execution.
- Timers: create and start repeating tasks using
UnityEngine.GameObjectcomponents orInvokeRepeating.
What NOT to put in Load
- Heavy synchronous I/O: loading a large database or downloading files from the internet blocks the server's main thread. RocketMod's
Load()is synchronous, and the server cannot process ticks until it returns. - Code that depends on other plugins: there is no guarantee that another plugin has been loaded before yours, even if your plugin physically appears first in the directory listing. RocketMod does not enforce plugin load order. If your plugin needs another plugin's functionality, use
RocketPluginManager.GetPlugin()with a null check and handle the missing-plugin case gracefully. - Throwing exceptions: if
Load()throws, RocketMod marks the plugin as failed and does not retry. Wrap critical initialization in try-catch and log the error so you can diagnose it from the log file.
OnPluginLoading event
The OnPluginLoading event is part of Rocket.Core.Plugins.Events. It fires during the Load() method, as part of the loading process. This is important for plugin framework systems that need to intercept or observe plugin loading as it happens.
csharp
using Rocket.Core.Plugins.Events;
using Rocket.Core.Plugins;
// Subscribe to the event (typically from another system or loader)
RocketPluginManager.OnPluginLoading += (plugin) =>
{
Rocket.Core.Logging.Logger.Log(
$"[Loader] Plugin loading: {plugin.Name}");
};The event fires at this specific point in the load sequence:
- Plugin configuration is loaded.
- Plugin
Load()begins executing. OnPluginLoadingfires — other systems can observe the loading plugin.- Plugin
Load()continues with the remaining initialization. Load()returns.- Plugin enters
Loadedstate.
Because OnPluginLoading fires inside Load(), any code that runs in response to this event executes during the plugin's own initialization. The plugin is not yet in the Loaded state, so some services may not be fully available.
When to hook OnPluginLoading
This event is used by cross-plugin systems, load balancers, and monitoring tools that need to track which plugins are loading. Individual plugins should not hook OnPluginLoading to run their own initialization — that code belongs in Load().
Active phase
After Load() completes without throwing, the plugin enters the Loaded state. It is now fully operational:
- Event handlers fire in response to server activity.
- Commands are registered and respond to player and console input.
- Configuration is readable through
Configuration.Instance. - The
Instancesingleton is set and accessible to command classes.
The plugin is not notified of entering Active
There is no OnPluginActivated event or method. The transition from Loaded to Active is implicit — it happens when Load() returns. If you need to execute code after all plugin initialization is complete but before the server processes the next tick, use a one-frame delay with Invoke:
csharp
protected override void Load()
{
// Schedule post-load initialization
Invoke(nameof(PostLoadInit), 0f);
}
private void PostLoadInit()
{
// This runs on the next Unity frame, after Load() has returned.
Rocket.Core.Logging.Logger.Log("[MyLifecyclePlugin] Post-load init.");
}This pattern is commonly used for plugins that need to query other plugins at startup, because it guarantees all plugins have finished loading before the deferred code runs.
Unload phase
Unload() is called when the plugin is being shut down — either because the server is stopping or because an administrator ran /rocket unload <name> or /rocket reload <name>.
csharp
protected override void Unload()
{
Rocket.Core.Logging.Logger.Log("[MyLifecyclePlugin] Unload() called.");
// Unsubscribe from events
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
UnturnedPlayerEvents.OnPlayerDisconnected -= OnPlayerDisconnected;
// Release singleton
Instance = null;
// Flush any pending state
Configuration.Save();
}Order of operations in Unload
OnPluginUnloadingfires — beforeUnload()is called but after the decision to unload has been made. Other systems can observe the unloading plugin while it is still fully operational.Unload()executes.- RocketMod removes the plugin's command registrations.
- The plugin enters
Unloadedstate. The instance is no longer referenced by RocketMod and becomes eligible for garbage collection.
What to put in Unload
- Event unsubscription: every
+=inLoad()must have a matching-=here. Missing unsubscriptions cause double-firing when the plugin is reloaded. - Singleton nullification: set
Instance = nullso stale references in command classes throw a clear error instead of silently operating on dead state. - Configuration save: if the plugin modified configuration values at runtime (e.g., tracking statistics that should persist), call
Configuration.Save()to flush them to disk. - Timer cancellation: stop any
InvokeRepeatingcalls or coroutines. Unity'sMonoBehaviourdestruction handles some cleanup, but explicit cancellation is more reliable. - File handle release: close any open file streams, database connections, or network sockets.
Common Unload mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Missing -= on events | After /rocket reload, event fires twice | Add the matching -= for every += |
Instance = null before event cleanup | Other code accessing Instance during event unsubscription gets null | Set Instance = null AFTER all unsubscriptions |
| Not saving configuration | Configuration changes lost on reload | Call Configuration.Save() in Unload() |
| Not canceling async operations | Orphaned threads continue after reload | Store a CancellationTokenSource and cancel it in Unload() |
Reload phase
/rocket reload <pluginname> triggers a full unload and load cycle on the same plugin instance:
Unload() → OnPluginUnloading fires → Unloaded → OnPluginLoading fires → Load() → LoadedThe reload operation is atomic from RocketMod's perspective: either both Unload() and Load() succeed, or the plugin is left in a safe state. If Unload() throws, the plugin is marked as failed and Load() is not called. If Load() throws after a successful Unload(), the plugin is left in the Unloaded state.
Reload safety checklist
Before supporting reload operations on your plugin, verify these invariants:
- Event parity: run a reload twice in a row and confirm the plugin works correctly after each reload. Double-firing is the most common reload bug.
- Memory growth: check the server's memory usage before and after five consecutive reloads. If memory grows, the plugin is leaking event subscriptions or object references.
- Configuration persistence: change a configuration value, reload the plugin, and confirm the new value is still active. If the configuration reset,
LoadDefaults()is overwriting the saved configuration.
Lifecycle events reference
RocketMod provides two lifecycle events through RocketPluginManager:
| Event | Fires | Purpose |
|---|---|---|
OnPluginLoading | During Load() | Cross-plugin load notification |
OnPluginUnloading | Before Unload() | Cross-plugin unload notification |
These events are static on RocketPluginManager and are not tied to any single plugin instance. They fire for every plugin load and unload on the server.
Subscribing to lifecycle events from another plugin
csharp
public class PluginMonitor : RocketPlugin<PluginMonitorConfiguration>
{
protected override void Load()
{
Rocket.Core.Plugins.RocketPluginManager.OnPluginLoading += OnAnyPluginLoading;
Rocket.Core.Plugins.RocketPluginManager.OnPluginUnloading += OnAnyPluginUnloading;
}
protected override void Unload()
{
Rocket.Core.Plugins.RocketPluginManager.OnPluginLoading -= OnAnyPluginLoading;
Rocket.Core.Plugins.RocketPluginManager.OnPluginUnloading -= OnAnyPluginUnloading;
}
private void OnAnyPluginLoading(IRocketPlugin plugin)
{
Rocket.Core.Logging.Logger.Log(
$"[Monitor] Plugin loading: {plugin.Name}");
}
private void OnAnyPluginUnloading(IRocketPlugin plugin)
{
Rocket.Core.Logging.Logger.Log(
$"[Monitor] Plugin unloading: {plugin.Name}");
}
}The event handlers receive the IRocketPlugin instance of the plugin being loaded or unloaded. You can cast it to the concrete type if you know the assembly, but doing so creates a tight coupling between plugins.
Plugin discovery timing
When the server starts, RocketMod discovers plugins in this order:
- RocketMod core initializes (configuration, permissions, translations).
- RocketMod scans
Rocket/Plugins/for.dllfiles. - For each DLL, RocketMod loads the assembly and searches for types inheriting from
RocketPlugin<TConfig>. - Each discovered plugin is instantiated and
Load()is called. - Plugins are loaded in file-system order (alphabetical by filename).
No built-in load order
RocketMod does not guarantee load order between plugins. If Plugin A depends on Plugin B being loaded first, the server operator must ensure this by naming conventions (e.g., 01_CorePlugin.dll, 02_MyPlugin.dll), but this is a convention, not a framework guarantee. To handle the general case, use a deferred initialization pattern:
csharp
protected override void Load()
{
Invoke(nameof(DeferredInit), 1f);
}
private void DeferredInit()
{
var otherPlugin = Rocket.Core.Plugins.RocketPluginManager
.GetPlugin("OtherPluginName") as OtherPlugin;
if (otherPlugin != null)
{
// Other plugin is available. Initialize cross-plugin features.
}
else
{
Rocket.Core.Logging.Logger.LogWarning(
"[MyPlugin] OtherPlugin not found. Cross-plugin features disabled.");
}
}The one-second delay ensures all plugins have finished loading before the deferred initialization runs. Adjust the delay based on your server's plugin load time.
Complete lifecycle diagram
Testing lifecycle behavior
Reload test
Write a test plugin that logs every lifecycle method call:
csharp
public class LifecycleTestPlugin : RocketPlugin<LifecycleTestPluginConfiguration>
{
private int _loadCount = 0;
public LifecycleTestPlugin()
{
Rocket.Core.Logging.Logger.Log("[LifecycleTest] Constructor.");
}
protected override void Load()
{
_loadCount++;
Rocket.Core.Logging.Logger.Log(
$"[LifecycleTest] Load() called. Load count: {_loadCount}");
}
protected override void Unload()
{
Rocket.Core.Logging.Logger.Log(
$"[LifecycleTest] Unload() called. Load count was: {_loadCount}");
}
}Deploy and run these commands in sequence:
/rocket reload LifecycleTestPlugin
/rocket reload LifecycleTestPlugin
/rocket reload LifecycleTestPluginThe expected log output shows three clean load-unload cycles with incrementing load counts. If the load count increases faster than expected, Unload() is not being called.
Crash recovery test
Force an exception in Load() and confirm the plugin enters the failed state:
csharp
protected override void Load()
{
throw new InvalidOperationException("Intentional crash in Load().");
}After deploying, the server log shows:
[LifecycleTestPlugin] Failed to load: Intentional crash in Load().The plugin appears in /rocket plugins with a failed indicator but is not operational. Running /rocket reload LifecycleTestPlugin does not retry the load — RocketMod only retries after a server restart.
Error handling across lifecycle
Load errors
If Load() throws an exception, RocketMod catches it, logs the error, and marks the plugin as failed. The plugin instance is NOT retained in the active plugin list, so no event handlers or commands from this plugin are registered. The assembly remains loaded in memory but no plugin code runs.
Unload errors
If Unload() throws, RocketMod logs the error but continues with the unload. The plugin is removed from the active list regardless of the exception. This means an Unload() exception can leave event subscriptions active if the exception occurs before the -= lines execute. Always structure Unload() with cleanup first, logging last:
csharp
protected override void Unload()
{
try
{
// Cleanup — this must run even if logging fails below
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
Instance = null;
Configuration.Save();
}
catch (Exception ex)
{
Rocket.Core.Logging.Logger.LogError(
$"[MyPlugin] Unload error (continuing): {ex.Message}");
}
}This ensures cleanup completes before any logging, which may itself throw if the logger is being shut down.
Multi-plugin lifecycle coordination
When multiple plugins are installed, their lifecycle methods execute sequentially. Plugin A's Load() runs to completion before Plugin B's Load() begins. The same applies to Unload() — plugins unload in reverse order of their load sequence.
Detecting peer plugin state from lifecycle
If your plugin needs to know whether another plugin has finished loading, use the deferred initialization pattern with a frame delay:
csharp
protected override void Load()
{
// Schedule peer detection after all plugins have loaded
Invoke(nameof(DetectPeers), 0f);
}
private void DetectPeers()
{
var target = RocketPluginManager.GetPlugin("EconomyPlugin");
if (target != null && target.IsLoaded)
{
Logger.Log("[MyPlugin] EconomyPlugin is available. Enabling economy features.");
_economyAvailable = true;
}
else
{
Logger.Log("[MyPlugin] EconomyPlugin not found. Economy features disabled.");
_economyAvailable = false;
}
}Lifecycle event forwarding
Some plugin architectures need a central coordinator that observes all plugin lifecycle events. The OnPluginLoading and OnPluginUnloading global events make this possible. A coordinator plugin subscribes to these events once (in its own Load) and forwards them to interested subsystems.
Graceful degradation when peers are missing
When your plugin depends on another plugin that may or may not be loaded, design the dependency as optional. Use a feature flag that disables dependent features when the peer is absent, rather than failing entirely.
Testing lifecycle behavior with a test harness
To validate that your plugin's lifecycle is correct, create a test plugin that measures timing and state transitions.
Lifecycle timing plugin
csharp
public class LifecycleTimer : RocketPlugin<LifecycleTimerConfiguration>
{
private DateTime _loadStart;
private DateTime _loadEnd;
private DateTime _unloadStart;
protected override void Load()
{
_loadStart = DateTime.UtcNow;
Logger.Log("[LifecycleTimer] Load started.");
// Simulate initialization work
System.Threading.Thread.Sleep(100);
_loadEnd = DateTime.UtcNow;
Logger.Log($"[LifecycleTimer] Load completed in " +
$"{( _loadEnd - _loadStart).TotalMilliseconds}ms.");
}
protected override void Unload()
{
_unloadStart = DateTime.UtcNow;
Logger.Log("[LifecycleTimer] Unload started.");
Logger.Log($"[LifecycleTimer] Plugin was active for " +
$"{( _unloadStart - _loadEnd).TotalSeconds}s.");
Logger.Log($"[LifecycleTimer] Unload completed in " +
$"{( DateTime.UtcNow - _unloadStart).TotalMilliseconds}ms.");
}
}Verifying event subscription parity
The most important lifecycle test is verifying that every += has a matching -=. Create a plugin that subscribes to an event and counts how many times it fires. Run two consecutive reloads. If the event fires twice as often after the second reload, you have a subscription leak.
csharp
private int _eventFireCount = 0;
private bool _wasReloaded = false;
protected override void Load()
{
UnturnedPlayerEvents.OnPlayerConnected += OnPlayerConnected;
Logger.Log("[LeakTest] Subscribed. Current count = " +
$"{_eventFireCount}");
if (_wasReloaded)
{
// If the count is higher than the number of players who actually joined,
// the subscription was not cleaned up during Unload.
Logger.Log("[LeakTest] WARNING: If count doubled since reload, " +
"subscription leak detected.");
}
}
protected override void Unload()
{
_wasReloaded = true;
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
}
private void OnPlayerConnected(UnturnedPlayer player)
{
_eventFireCount++;
Logger.Log($"[LeakTest] Event fired. Total: {_eventFireCount}");
}Performance profiling during lifecycle
Load time benchmarks
A plugin's Load() runs on the main thread and blocks server ticks until it returns. Measure your load time and keep it under 500ms for a good player experience. If load time exceeds 2000ms (2 seconds), players may time out during server startup.
| Load time | Impact | Recommendation |
|---|---|---|
| < 100ms | Imperceptible | No action needed |
| 100ms — 500ms | Noticeable during server start | Acceptable for most plugins |
| 500ms — 2000ms | Server startup delay | Optimize — defer heavy I/O |
| > 2000ms | Players may time out | Move work to deferred initialization |
Deferred initialization pattern for slow operations
When your plugin needs to load a large configuration, populate a cache, or validate network resources, defer the heavy work:
csharp
protected override void Load()
{
// Fast — completes immediately
Instance = this;
Logger.Log("[MyPlugin] Core initialization complete.");
// Schedule heavy work one frame later
Invoke(nameof(DeferredHeavyInit), 0f);
}
private void DeferredHeavyInit()
{
Logger.Log("[MyPlugin] Loading database...");
// Database connection, cache population, network calls
Logger.Log("[MyPlugin] Heavy initialization complete.");
}This pattern gets the plugin registered and operational within the first tick, then performs slow operations after the server is already responsive.
Frequently asked questions
Does OnPluginLoading fire during Load or after it?
OnPluginLoading fires during Load(), as part of the loading process. It is invoked by the RocketMod framework while Load() is executing, providing an observation point for cross-plugin systems. Plugin authors should not rely on this event for their own initialization — Load() is the correct place for that.
What happens if I do not call Configuration.Save() in Unload?
The configuration values are cached in memory but not written to disk. If the plugin modified any configuration values at runtime (e.g., tracking an internal counter stored in config), those changes are lost on reload or server restart. The configuration file on disk retains the values from the last Save() call. Call Configuration.Save() in Unload() if your plugin mutates configuration values at runtime.
Can another plugin prevent mine from loading?
No. Each plugin's Load() is independent. If Plugin A throws in Load(), only Plugin A is marked as failed. Other plugins in the same server load normally, even if they were discovered after the failing plugin.
Is there a guaranteed load order?
No. RocketMod discovers plugins in file-system order, which is effectively alphabetical by filename on most file systems. There is no dependency-declaration system like OpenMod's [PluginDependency]. If your plugin requires another plugin, defer the dependent initialization by one frame using Invoke and check RocketPluginManager.GetPlugin().
Does rocket reload preserve the plugin instance?
Yes. RocketMod calls Unload() followed by Load() on the same instance. The object is not destroyed between calls. This means instance fields (like _loadCount in the test example above) persist across reload cycles. If you need a truly fresh state on each load, reset all fields at the start of Load().
Unload guarantees and timing
RocketMod guarantees that Unload() is called in these scenarios:
| Scenario | Unload called? | Notes |
|---|---|---|
| Server shutdown (clean) | Yes | RocketMod calls Unload on every loaded plugin |
| Server crash | No | Process terminates without cleanup |
/rocket unload <plugin> | Yes | Immediate unload of the specified plugin |
/rocket reload <plugin> | Yes | Followed immediately by Load() on same instance |
/rocket reload * (all) | Yes | All plugins unloaded and reloaded in sequence |
| Plugin throws in constructor | No | Plugin instance never entered Loaded state |
| Plugin throws in Load() | Yes (via reload) | Unload called only if reload is triggered |
Unload timeout
RocketMod does not enforce a timeout on Unload(). If Unload() hangs (e.g., waiting on a network resource that never responds), the plugin never finishes unloading, and subsequent operations (server shutdown, reload of other plugins) are blocked indefinitely. Always use a timeout pattern for unload operations that could block:
csharp
protected override void Unload()
{
var cancellation = new CancellationTokenSource(TimeSpan.FromSeconds(5));
try
{
// Attempt cleanup with timeout
Task.Run(() => FlushPendingWrites(), cancellation.Token)
.Wait(cancellation.Token);
}
catch (OperationCanceledException)
{
Logger.LogWarning("[MyPlugin] Unload timed out. Continuing.");
}
// Always run synchronous cleanup regardless of timeout
UnturnedPlayerEvents.OnPlayerConnected -= OnPlayerConnected;
Instance = null;
}Document history
| Version | Date | Author | Notes |
|---|---|---|---|
| 1.0 | 2026-07-27 | 57 Studios | Initial publication. Full lifecycle reference. |
