Inter-Plugin Communication
As your RocketMod plugin library grows, individual plugins need to coordinate with each other. An economy plugin needs to tell a shop plugin that a player has enough currency. A teleportation plugin needs to tell a combat plugin to prevent teleportation during PvP. A logging plugin needs to listen to events from every other plugin on the server.
RocketMod provides several mechanisms for inter-plugin communication: dependency resolution through RocketPluginManager, static C# events, shared library assemblies, and direct method invocation through the RocketPlugin class. This article covers each pattern with working code examples and explains when to use each approach.
Prerequisites
- Articles 1 through 7 (Fundamentals track), especially article 4 (Understanding the Permission System) and article 6 (Event Subscription and Lifecycle).
- Article 7 (Plugin Dependencies and Load Order) for understanding the load order constraints.
- A multi-plugin server setup with at least two RocketMod plugins for testing.
What you will learn
- How to check if another plugin is loaded using
RocketPluginManager.GetPlugin()and theRocketPlugin.IsDependencyLoaded()instance method. - How to use the
RocketPlugin.Stateproperty to inspect plugin load state. - How to use RocketMod's static C# events for plugin-to-plugin event communication.
- How to create shared library projects that multiple plugins can reference.
- How to use the
RocketPlugin.ExecuteDependencyCode()instance method for safe cross-plugin calls. - How to handle load order dependencies and prevent stale references.
- Best practices for designing plugin APIs that are consumable by other plugins.
Plugin discovery
Before you can communicate with another plugin, you need to know whether it is loaded. RocketMod provides two real mechanisms for plugin discovery: the RocketPluginManager class and the RocketPlugin.IsDependencyLoaded() instance method.
RocketPluginManager
The RocketPluginManager class manages all loaded plugins on the server. Its GetPlugin method returns an IRocketPlugin reference for the requested plugin. Its GetPlugins method returns a List<IRocketPlugin> of every loaded plugin.
RocketPluginManager also exposes the OnPluginsLoaded event, which fires after all plugins have finished loading. This is the appropriate point to run dependency checks that need every plugin to be present.
IsDependencyLoaded
IsDependencyLoaded is an instance method on the RocketPlugin class. It returns a bool indicating whether a given dependency is present. Because it is an instance method, you call it from inside your plugin class -- your plugin is the dependency consumer, and this method checks whether the plugins your plugin depends on are loaded:
csharp
using Rocket.Core.Plugins;
using Rocket.Core.Logging;
public class ShopPlugin : RocketPlugin<ShopConfiguration>
{
private void CheckDependencies()
{
if (!IsDependencyLoaded(/* dependency identifier */))
{
Logger.Log("Required dependency is not loaded.");
}
}
}Checking plugin state
The RocketPlugin class has a State property of type PluginState (an enum in the Rocket.API namespace). You can use this to determine whether a plugin is loaded, unloading, or in an error state.
Calling methods on another plugin
Once you hold a reference to another plugin -- obtained through RocketPluginManager.GetPlugin -- you can interact with it. The cleanest approach is to define a shared interface that both plugins know about.
Interface-based communication
Create a shared interface in a common library:
csharp
public interface IEconomyPlugin
{
bool HasBalance(ulong playerSteamId, decimal amount);
bool Withdraw(ulong playerSteamId, decimal amount);
bool Deposit(ulong playerSteamId, decimal amount);
decimal GetBalance(ulong playerSteamId);
}The economy plugin implements the interface:
csharp
public class EconomyPlugin : RocketPlugin<EconomyConfiguration>, IEconomyPlugin
{
public bool HasBalance(ulong playerSteamId, decimal amount)
{
return true;
}
public bool Withdraw(ulong playerSteamId, decimal amount)
{
return true;
}
public bool Deposit(ulong playerSteamId, decimal amount)
{
return true;
}
public decimal GetBalance(ulong playerSteamId)
{
return 100m;
}
}The consuming plugin resolves the interface by obtaining the plugin instance through RocketPluginManager.GetPlugin, which returns an IRocketPlugin. It then casts to the known interface:
csharp
using Rocket.Core.Plugins;
using Rocket.Core.Logging;
public class ShopPlugin : RocketPlugin<ShopConfiguration>
{
private IEconomyPlugin economy;
protected void ResolveEconomy()
{
var pluginRef = /* obtain via RocketPluginManager.GetPlugin */;
if (pluginRef is IEconomyPlugin eco)
{
economy = eco;
Logger.Log("EconomyPlugin resolved successfully.");
}
else
{
Logger.Log("EconomyPlugin not available.");
}
}
public bool PurchaseItem(ulong playerSteamId, decimal price)
{
if (economy == null)
return false;
if (!economy.HasBalance(playerSteamId, price))
return false;
return economy.Withdraw(playerSteamId, price);
}
}The specific access path to the RocketPluginManager instance depends on how your plugin is structured. The RocketPluginManager class exists in the Rocket.Core.Plugins namespace with the methods described above.
Using ExecuteDependencyCode
For cases where the interface is not available at compile time, the RocketPlugin class provides ExecuteDependencyCode. This is an instance method on RocketPlugin, meaning your plugin can call it to execute code that requires another plugin to be present. It returns void and wraps the execution safely -- if the dependency is unavailable, the code block is not executed:
csharp
using Rocket.Core.Plugins;
public void ChargePlayer(ulong steamId, decimal amount)
{
ExecuteDependencyCode(/* dependency identifier */, () =>
{
// Code here only runs if the dependency is loaded.
// Access the dependency plugin through RocketPluginManager.
});
}The ExecuteDependencyCode method ensures your plugin does not crash when a dependency is missing. It gates execution behind the dependency check so the body only runs when the required plugin is present.
Static C# events
RocketMod plugins can fire custom events that other plugins subscribe to using standard C# static events. This is the loosest coupling approach -- the sending plugin does not know which plugins are listening.
Defining a custom event
csharp
public class PlayerPurchasedItemEvent
{
public ulong PlayerSteamId { get; }
public string ItemName { get; }
public decimal Price { get; }
public PlayerPurchasedItemEvent(ulong playerSteamId, string itemName, decimal price)
{
PlayerSteamId = playerSteamId;
ItemName = itemName;
Price = price;
}
}Firing a custom event
The sending plugin exposes a static event. It does not need to reference other plugin types:
csharp
public class ShopPlugin : RocketPlugin<ShopConfiguration>
{
public static event Action<PlayerPurchasedItemEvent> OnPlayerPurchasedItem;
public void ProcessPurchase(UnturnedPlayer player, string itemName, decimal price)
{
OnPlayerPurchasedItem?.Invoke(new PlayerPurchasedItemEvent(
player.CSteamID.m_SteamID, itemName, price));
}
}Subscribing to a custom event
The receiving plugin subscribes directly to the static event on the sending plugin's type. No dynamic lookup is needed because the type reference is known at compile time:
csharp
using Rocket.Core.Logging;
public class LoggingPlugin : RocketPlugin<LoggingConfiguration>
{
protected override void LoadPlugin()
{
ShopPlugin.OnPlayerPurchasedItem += OnPurchase;
}
protected override void UnloadPlugin()
{
ShopPlugin.OnPlayerPurchasedItem -= OnPurchase;
}
private void OnPurchase(PlayerPurchasedItemEvent purchaseEvent)
{
Logger.Log(
$"Player {purchaseEvent.PlayerSteamId} purchased {purchaseEvent.ItemName} for {purchaseEvent.Price}");
}
}Event lifecycle considerations
- Subscribe in
LoadPlugin()and desubscribe inUnloadPlugin(). - If the sending plugin is unloaded and reloaded, its static event field is reset. Subscribing plugins must re-subscribe.
- If a subscribing plugin is unloaded without desubscribing, the next event fire will attempt to invoke a stale delegate, causing a
NullReferenceExceptionin the sending plugin.
Shared library approach
For large plugin ecosystems, create a shared library assembly that both plugins reference. The shared library contains interfaces, data transfer objects, and event types.
Shared library project structure
MyServerShared/
├── MyServerShared.csproj
├── Interfaces/
│ ├── IEconomyPlugin.cs
│ └── IShopPlugin.cs
├── Events/
│ └── PlayerPurchasedItemEvent.cs
└── Models/
└── PurchaseResult.csThe .csproj targets .NET Framework 4.7.2 and does not reference RocketMod directly -- only interfaces and models:
xml
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net472</TargetFramework>
</PropertyGroup>
</Project>Referencing the shared library
Each plugin adds a project reference or DLL reference to the shared library:
xml
<ItemGroup>
<Reference Include="MyServerShared">
<HintPath>..\shared\bin\Release\MyServerShared.dll</HintPath>
<Private>false</Private>
</Reference>
</ItemGroup>The Private flag is set to false to prevent the shared DLL from being copied into each plugin's output directory. The shared DLL is placed in Rocket/Libraries/ once, and all plugins resolve it from there.
Shared library deployment
Copy the shared library to Rocket/Libraries/ on the server:
Rocket/Libraries/MyServerShared.dll
Rocket/Plugins/EconomyPlugin.dll
Rocket/Plugins/ShopPlugin.dllBoth plugins load the shared library from the Libraries folder at runtime.
Load order dependencies
When Plugin A depends on Plugin B, Plugin B must load first. RocketMod loads plugins in alphabetical order by assembly name by default. To enforce a load order, rename the assemblies with a prefix:
00_EconomyPlugin.dll
01_ShopPlugin.dll
02_LoggingPlugin.dllPlugins numbered with a leading zero load before plugins with a higher number. This is a convention, not a RocketMod API feature, but it is reliable because RocketMod uses directory enumeration order on most file systems.
Runtime dependency check
In the dependent plugin's LoadPlugin() method, check that the dependency is available through RocketPluginManager.GetPlugin and log a warning if it is not:
csharp
using Rocket.Core.Plugins;
using Rocket.Core.Logging;
protected override void LoadPlugin()
{
var economy = /* obtain via RocketPluginManager.GetPlugin */;
if (economy == null)
{
Logger.Log("EconomyPlugin not found. Shop functions will be disabled.");
}
}Best practices
- Prefer events over direct calls. Events create loose coupling. The sending plugin does not need to know which plugins are listening. Adding a new listener does not require changes to the sending plugin.
- Use interfaces for performance-critical paths. Direct interface calls are faster than event dispatch. Use interfaces when the call path is invoked frequently -- for example, every time a player purchases an item.
- Avoid cross-plugin coupling in configuration. Do not make one plugin read another plugin's configuration file directly. Use the API methods provided by the plugin.
- Document the cross-plugin contract. If Plugin A calls Plugin B's methods, document which methods are part of the public API and which are internal. Internal methods can change without notice.
- Version the shared library. When the shared library changes, all plugins that reference it must be recompiled and redeployed together. Use assembly versioning to detect mismatches.
Versioning strategies
When multiple plugins share a common interface library, version mismatches cause runtime failures. Use these strategies to prevent them.
Assembly version in shared libraries
Set the assembly version explicitly in the shared library's .csproj:
xml
<PropertyGroup>
<Version>1.0.0.0</Version>
<FileVersion>1.0.0.0</FileVersion>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
</PropertyGroup>Increment the version when the interface changes. All plugins must be recompiled against the new version. If a plugin compiled against version 1.0.0.0 loads alongside a plugin compiled against version 1.1.0.0, the runtime loads one version of the shared library -- whichever loads first -- and the other plugin may fail with a TypeLoadException or MissingMethodException.
Interface version check
Add a static version field to your shared interface:
csharp
public interface IEconomyPlugin
{
public const int Version = 2;
bool HasBalance(ulong playerSteamId, decimal amount);
bool Withdraw(ulong playerSteamId, decimal amount);
bool Deposit(ulong playerSteamId, decimal amount);
decimal GetBalance(ulong playerSteamId);
}The consuming plugin checks the version when resolving the interface:
csharp
var pluginRef = /* obtain via RocketPluginManager.GetPlugin */;
if (pluginRef is IEconomyPlugin eco && IEconomyPlugin.Version >= 2)
{
economy = eco;
}
else
{
Logger.Log("Economy plugin version is too old. Expected interface version 2.");
}Breaking changes
When you change a shared interface, all plugins that implement or consume it must be updated. Breaking changes include:
- Adding a required method to the interface.
- Changing the parameter types of an existing method.
- Removing a method from the interface.
- Changing the return type of an existing method.
Additive changes (new optional methods) can be backward compatible if consuming plugins check for the method's existence via reflection before calling it.
Frequently asked questions
How do I get a reference to another plugin?
Use RocketPluginManager.GetPlugin, which returns an IRocketPlugin. Cast the result to your plugin's type or to a shared interface to access its methods and properties. RocketPluginManager.GetPlugins returns List<IRocketPlugin> for enumerating all loaded plugins.
Can two plugins communicate without a shared library?
Yes. Use static C# events. The sending plugin exposes a static event on its type, and the receiving plugin subscribes to it directly by referencing the sending plugin's type at compile time.
What happens if a dependency plugin is unloaded while another plugin is using it?
If a plugin is unloaded, its static event fields are cleared and its instance is no longer valid. The dependent plugin should check the dependency reference before each use, not just at load time. Use RocketPlugin.State to inspect whether a plugin is still in a loaded state.
Can I call methods on an unloaded plugin?
No. The plugin instance is disposed on unload. Any attempt to call methods on a disposed plugin will throw an exception or silently fail depending on the method.
How do I prevent circular dependencies?
Design the dependency graph as a directed acyclic graph. Plugin A depends on Plugin B, but Plugin B must not depend on Plugin A. If bidirectional communication is needed, use events -- Plugin A fires an event that Plugin B listens to, and Plugin B fires a different event that Plugin A listens to.
Cross-references
- Error Handling and Logging -- the next article; handling exceptions from cross-plugin calls.
- Scheduling Tasks and Background Work -- the previous article; scheduling cross-plugin notifications.
- Plugin Dependencies and Load Order -- load order configuration for dependent plugins.
- Commands and IRocketCommand -- command-related patterns.
- Event Subscription and Lifecycle -- event patterns for cross-plugin communication.
What changed in this revision
- Removed all references to
R.Plugins-- theRclass has noPluginsproperty in the real API surface. - Removed
R.Plugins.GetPlugin<T>()usage --GetPluginexists onRocketPluginManager, not on a property ofR. - Removed
R.Plugins.IsDependencyLoaded(string)usage --IsDependencyLoadedis an instance method onRocketPlugin(returnsbool), not a static method onR.Plugins. - Removed
R.Plugins.ExecuteDependencyCode(string, ...)usage --ExecuteDependencyCodeis an instance method onRocketPlugin(returnsvoid), not a static method onR.Plugins. - Removed entire "Plugin messaging via console commands" section -- the
R.Commandsproperty does not exist on theRclass. - Removed the "Test command for discovery" debug command section -- it depended on
R.Plugins.IsDependencyLoaded(). - Changed code samples from
Load()/Unload()toLoadPlugin()/UnloadPlugin()-- these are the RocketMod lifecycle method names present in the API surface. - Added correct API references:
RocketPluginManager.GetPlugin(returnsIRocketPlugin),RocketPluginManager.GetPlugins(returnsList<IRocketPlugin>),RocketPluginManager.OnPluginsLoadedevent,RocketPlugin.IsDependencyLoadedinstance method,RocketPlugin.ExecuteDependencyCodeinstance method,RocketPlugin.Stateproperty (typePluginState). - Updated code samples to use casts from
IRocketPluginto interface types, reflecting the realGetPluginreturn type. - Removed
R.Translate()usages from code samples that were not directly relevant to inter-plugin communication.
