Skip to content

Plugin Examples

Tsu edited this page Jun 10, 2026 · 2 revisions

All examples below are complete, compilable plugins. Reference Plugin Development for project setup and the full API surface.


Hub fallback

Sends new players to hub as the default backend, unless the registry provides an override. Useful if your proxy config does not have a static try list or you want code-driven routing.

using Nimbus.Proxy;

public sealed class HubFallbackPlugin : IPlugin
{
    public string Name    => "hub-fallback";
    public string Version => "1.0.0";

    private IProxyApi _api = null!;

    public void Initialize(IProxyApi api)
    {
        _api = api;
        api.Events.Subscribe<PlayerChooseInitialServerEvent>(async e =>
        {
            if (e.Target != null) return; // already assigned

            var hub = await api.ResolveServerAsync("hub", default);
            if (hub != null)
                e.Target = hub;
            else
                e.Cancel("No servers are available right now.");
        });
    }

    public void Shutdown() { }
}

IP allowlist

Blocks connections from IPs not in a hard-coded list. Checks at TCP accept time, before any VS packets are exchanged.

using Nimbus.Proxy;

public sealed class IpAllowlistPlugin : IPlugin
{
    public string Name    => "ip-allowlist";
    public string Version => "1.0.0";

    // Edit this list or load it from a config file.
    private static readonly HashSet<string> Allowed = new(StringComparer.Ordinal)
    {
        "192.168.1.100",
        "10.0.0.50",
    };

    public void Initialize(IProxyApi api)
    {
        api.Events.Subscribe<PlayerConnectEvent>(e =>
        {
            if (!Allowed.Contains(e.Player.ClientRemote))
                e.Deny("Connection refused.");
            return Task.CompletedTask;
        });
    }

    public void Shutdown() { }
}

Transfer logger

Appends every player transfer to a CSV file for auditing.

using Nimbus.Proxy;

public sealed class TransferLoggerPlugin : IPlugin
{
    public string Name    => "transfer-logger";
    public string Version => "1.0.0";

    private const string LogPath = "transfer-log.csv";
    private static readonly object FileLock = new();

    public void Initialize(IProxyApi api)
    {
        if (!File.Exists(LogPath))
            File.WriteAllText(LogPath, "timestamp,session_id,player,uid,from,to,mode\n");

        api.Events.Subscribe<PlayerTransferredEvent>(e =>
        {
            var row = string.Join(",",
                DateTimeOffset.UtcNow.ToString("o"),
                e.Player.Id,
                e.Player.Name ?? "?",
                e.Player.Uid ?? "?",
                e.From?.ServerId ?? "initial",
                e.To.ServerId,
                e.Mode);

            lock (FileLock)
                File.AppendAllText(LogPath, row + "\n");

            return Task.CompletedTask;
        });
    }

    public void Shutdown() { }
}

Kick notifier

Sends an HTTP webhook when a backend kicks a player. Useful for alerting on unexpected server crashes.

using System.Net.Http;
using System.Text;
using System.Text.Json;
using Nimbus.Proxy;

public sealed class KickNotifierPlugin : IPlugin
{
    public string Name    => "kick-notifier";
    public string Version => "1.0.0";

    // Replace with your actual webhook URL.
    private const string WebhookUrl = "https://example.com/hooks/nimbus";
    private static readonly HttpClient Http = new();

    public void Initialize(IProxyApi api)
    {
        api.Events.Subscribe<ServerKickedEvent>(async e =>
        {
            var payload = JsonSerializer.Serialize(new
            {
                text    = $":warning: **{e.Player.Name ?? e.Player.Uid ?? "?"}** was kicked by `{e.Server.ServerId}`",
                session = e.Player.Id,
                server  = e.Server.ServerId,
                ts      = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
            });

            try
            {
                await Http.PostAsync(WebhookUrl,
                    new StringContent(payload, Encoding.UTF8, "application/json")).ConfigureAwait(false);
            }
            catch (Exception ex)
            {
                api.LogWarn($"[kick-notifier] webhook failed: {ex.Message}");
            }
        });
    }

    public void Shutdown() { }
}

Load balancer

On initial connect, sends each player to the backend with the fewest current sessions. Falls back to hub if the registry is unavailable.

using Nimbus.Proxy;

public sealed class LoadBalancerPlugin : IPlugin
{
    public string Name    => "load-balancer";
    public string Version => "1.0.0";

    private IProxyApi _api = null!;
    private static readonly string[] Pool = ["hub", "world-a", "world-b"];

    public void Initialize(IProxyApi api)
    {
        _api = api;
        api.Events.Subscribe<PlayerChooseInitialServerEvent>(async e =>
        {
            IServerInfo? best = null;
            int bestCount = int.MaxValue;

            foreach (var id in Pool)
            {
                var server = await api.ResolveServerAsync(id, default);
                if (server == null) continue;

                // Count sessions currently on this backend.
                int count = api.Players.Count(p => p.CurrentServer?.ServerId == id);
                if (count < bestCount)
                {
                    bestCount = count;
                    best = server;
                }
            }

            if (best != null)
                e.Target = best;
            else
                e.Cancel("No servers available.");
        });
    }

    public void Shutdown() { }
}

Maintenance mode (flag file)

Reads a flag file at a configurable path. When the file exists, new connections are held at the gate. Uses ServerPreConnectEvent so it applies on both initial connect and transfers.

using Nimbus.Proxy;

public sealed class MaintenanceModePlugin : IPlugin
{
    public string Name    => "maintenance-mode";
    public string Version => "1.0.0";

    private const string FlagFile = "maintenance.flag";

    public void Initialize(IProxyApi api)
    {
        api.Events.Subscribe<ServerPreConnectEvent>(e =>
        {
            if (File.Exists(FlagFile))
                e.Cancel("The server is down for maintenance. Please try again later.");
            return Task.CompletedTask;
        });
    }

    public void Shutdown() { }
}

Create the flag to activate:

echo "" > maintenance.flag

Remove it to deactivate - no reload needed, the plugin checks on every connection attempt.


Per-player UID routing

Routes specific players to a dedicated backend based on a stored allowlist. Useful for staff, beta testers, or VIP access.

using System.Collections.Frozen;
using Nimbus.Proxy;

public sealed class UidRouterPlugin : IPlugin
{
    public string Name    => "uid-router";
    public string Version => "1.0.0";

    // UID → target server ID
    private static readonly FrozenDictionary<string, string> Routes =
        new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
        {
            ["ABCDEF1234"] = "staff-world",
            ["DEADBEEF00"] = "beta-world",
        }.ToFrozenDictionary();

    public void Initialize(IProxyApi api)
    {
        api.Events.Subscribe<PlayerChooseInitialServerEvent>(async e =>
        {
            var uid = e.Player.Uid;
            if (uid == null) return;
            if (!Routes.TryGetValue(uid, out var serverId)) return;

            var target = await api.ResolveServerAsync(serverId, default);
            if (target != null)
            {
                e.Target = target;
                api.LogInfo($"[uid-router] {e.Player.Name ?? uid}{serverId}");
            }
        });
    }

    public void Shutdown() { }
}

Session stats on disconnect

Logs a human-readable session summary for every player disconnect. Similar to the built-in proxy log, but with a custom format and written via the plugin logger so it can be redirected separately.

using Nimbus.Proxy;

public sealed class SessionStatsPlugin : IPlugin
{
    public string Name    => "session-stats";
    public string Version => "1.0.0";

    private readonly Dictionary<long, DateTimeOffset> _starts = new();
    private IProxyApi _api = null!;

    public void Initialize(IProxyApi api)
    {
        _api = api;

        api.Events.Subscribe<ServerPostConnectEvent>(e =>
        {
            if (e.Previous == null) // initial connect only
                _starts[e.Player.Id] = DateTimeOffset.UtcNow;
            return Task.CompletedTask;
        });

        api.Events.Subscribe<PlayerDisconnectEvent>(e =>
        {
            if (!_starts.TryGetValue(e.Player.Id, out var start))
                return Task.CompletedTask;

            _starts.Remove(e.Player.Id);
            var duration = DateTimeOffset.UtcNow - start;
            var minutes  = (int)duration.TotalMinutes;
            var seconds  = duration.Seconds;

            api.LogInfo(
                $"[session-stats] {e.Player.Name ?? "?"} " +
                $"session={minutes}m{seconds:D2}s " +
                $"↑{FormatBytes(e.BytesC2S)}{FormatBytes(e.BytesS2C)}");

            return Task.CompletedTask;
        });
    }

    public void Shutdown() => _starts.Clear();

    private static string FormatBytes(long b) =>
        b >= 1_048_576 ? $"{b / 1_048_576.0:F1}MB" :
        b >= 1024      ? $"{b / 1024.0:F1}KB" :
                         $"{b}B";
}

Clone this wiki locally