Shared API
Shared API lets plugins expose typed interfaces that other plugins can consume at runtime, via APIIInterfaceManager.
Shared Interface Lifecycle
Shared API integration happens through three APIBasePlugin callbacks, called in this order across every loaded plugin. They are declared by APIIPlugin and implemented as virtual no-ops on APIBasePlugin, so override only the ones you need:
- APIBasePlugin.ConfigureSharedInterface: register the interfaces your plugin provides. Runs for every plugin first.
- APIBasePlugin.UseSharedInterface: retrieve interfaces from other plugins. Runs for every plugin only after all have finished registering.
- APIBasePlugin.OnSharedInterfaceInjected: react once shared interfaces are resolved. Runs last, again across every plugin.
public override void ConfigureSharedInterface(IInterfaceManager interfaceManager) { /* register provider interfaces */ }
public override void UseSharedInterface(IInterfaceManager interfaceManager) { /* resolve dependencies from other plugins */ }
public override void OnSharedInterfaceInjected(IInterfaceManager interfaceManager) { /* optional: react once everyone has resolved */ }This sequence re-runs whenever the plugin set changes (e.g. hot reload
adds/removes a plugin) - don't assume
ConfigureSharedInterface/UseSharedInterface only fire once at startup.
Providing Shared Interfaces
Use APIIInterfaceManager.AddSharedInterface in ConfigureSharedInterface:
public override void ConfigureSharedInterface(IInterfaceManager interfaceManager)
{
interfaceManager.AddSharedInterface<IEconomyApi, EconomyApi>("Economy.Api.v1", new EconomyApi());
}TInterface is what consumers resolve and TImpl the concrete implementation registered under the key, so AddSharedInterface requires TInterface to be a class and TImpl to be a class implementing it (where TInterface : class, where TImpl : class, TInterface).
The interface type (IEconomyApi here) needs to live somewhere both plugins
can reference - typically a small contracts-only assembly the provider ships
alongside itself, referenced by consumers like a regular package.
Consuming Shared Interfaces
APIIInterfaceManager.TryGetSharedInterface is preferred
for optional dependencies - it returns false instead of failing, and leaves
the out parameter null when the key isn't registered;
APIIInterfaceManager.GetSharedInterface returns the
interface directly and doesn't report a missing key, so use it for mandatory
dependencies whose provider is always present;
APIIInterfaceManager.HasSharedInterface is a quick
presence check.
private IEconomyApi? _economyApi;
public override void UseSharedInterface(IInterfaceManager interfaceManager)
{
if (!interfaceManager.TryGetSharedInterface<IEconomyApi>("Economy.Api.v1", out _economyApi))
Core.Logger.LogWarning("Economy API not available. Economy features disabled.");
}
// Mandatory dependency: IEconomyApi economyApi = interfaceManager.GetSharedInterface<IEconomyApi>("Economy.Api.v1");
// Presence check: bool hasEconomy = interfaceManager.HasSharedInterface("Economy.Api.v1");APIIInterfaceManager Members
TInterface is class-constrained on every generic member:
| Member | Signature | Returns |
|---|---|---|
| APIIInterfaceManager.AddSharedInterface | void AddSharedInterface<TInterface, TImpl>(string key, TImpl implInstance) | - |
| APIIInterfaceManager.TryGetSharedInterface | bool TryGetSharedInterface<TInterface>(string key, out TInterface? instance) | true when the key is registered; instance is then non-null (annotated [NotNullWhen(true)]) |
| APIIInterfaceManager.GetSharedInterface | TInterface GetSharedInterface<TInterface>(string key) | the implementation registered under key |
| APIIInterfaceManager.HasSharedInterface | bool HasSharedInterface(string key) | whether a key is registered |
Complete Example
A contracts-only interface, referenced by both the provider and consumer plugin:
namespace MyPlugin.Contracts;
public interface IEconomyApi
{
int GetBalance(ulong steamId);
bool TrySpend(ulong steamId, int amount);
}Provider registers its implementation; consumer resolves it and uses it (Load/Unload omitted for brevity):
// Provider plugin
public sealed class EconomyApi : IEconomyApi
{
private readonly Dictionary<ulong, int> _balances = new();
public int GetBalance(ulong steamId) => _balances.TryGetValue(steamId, out int balance) ? balance : 0;
public bool TrySpend(ulong steamId, int amount)
{
int current = GetBalance(steamId);
if (current < amount) return false;
_balances[steamId] = current - amount;
return true;
}
}
public sealed class EconomyPlugin(ISwiftlyCore core) : BasePlugin(core)
{
private readonly EconomyApi _api = new();
public override void ConfigureSharedInterface(IInterfaceManager interfaceManager) =>
interfaceManager.AddSharedInterface<IEconomyApi, EconomyApi>("Economy.Api.v1", _api);
}
// Consumer plugin
public sealed class ShopPlugin(ISwiftlyCore core) : BasePlugin(core)
{
private IEconomyApi? _economyApi;
public override void UseSharedInterface(IInterfaceManager interfaceManager) =>
interfaceManager.TryGetSharedInterface<IEconomyApi>("Economy.Api.v1", out _economyApi);
public void TryBuy(IPlayer player, int cost)
{
if (_economyApi == null) { player.SendChat("Economy service is unavailable."); return; }
player.SendChat(_economyApi.TrySpend(player.SteamID, cost) ? "Purchase successful." : "Not enough balance.");
}
}Best Practices
- Use stable, descriptive keys such as
PluginName.ServiceName.v1, and version them on breaking changes (.v1->.v2). - Prefer
TryGetSharedInterfacefor optional dependencies so a missing provider doesn't crash yours. - Keep shared contracts small and implementation-agnostic - the contract assembly is the only thing both plugins should share.
- Since registration can re-run on hot reload, unsubscribe before re-subscribing to provider events in
UseSharedInterface, or you'll get duplicate handlers.