Recipes
Common integration patterns — each snippet is self-contained and copy-pasteable.
1. React to an alarm by flipping a switch
Listen for a paired alarm via RustPlusFcm, then turn on a siren switch via RustPlus when it
fires. The FCM listener and the companion client run in parallel: the FCM socket stays open
indefinitely while the RustPlus connection is opened on demand for the flip.
using RustPlusApi;
using RustPlusApi.Fcm;
using RustPlusApi.Fcm.Data.Events;
using RustPlusApi.Fcm.Registration;
// ── constants ────────────────────────────────────────────────────────────────
const string ConfigPath = "rustplus.config.json";
const string ServerIp = "192.0.2.1";
const int ServerPort = 28082; // companion port from pairing, not game port
const ulong PlayerId = 76561198000000000UL;
const int PlayerToken = -123456789;
const ulong SirenSwitchId = 1234567UL; // entity ID of your siren smart switch
// ── FCM listener ─────────────────────────────────────────────────────────────
var credentials = CredentialsStore.Load(ConfigPath);
using var listener = new RustPlusFcm(credentials);
listener.OnAlarmTriggered += async (_, alarm) =>
{
Console.WriteLine($"Alarm fired: {alarm?.Title} — turning on siren switch");
using var rustPlus = new RustPlus(new RustPlusConnection(ServerIp, ServerPort, PlayerId, PlayerToken));
await rustPlus.ConnectAsync();
var result = await rustPlus.SetSmartSwitchValueAsync(SirenSwitchId, true);
if (!result.IsSuccess)
Console.Error.WriteLine($"Switch failed: {result.Error?.Message}");
};
await listener.ConnectAsync();
Console.WriteLine("Listening for alarms — press Ctrl+C to exit.");
await Task.Delay(Timeout.Infinite);
Note
OnAlarmTriggered only fires for alarms that have been paired in game via the Rust+ app.
See FCM Notifications for the full event surface and reconnect strategy.
2. Save the server map to disk
GetMapAsync returns a ServerMap whose JpgImage property contains the raw JPEG bytes. Cache
this response — the map is large and rarely changes.
using RustPlusApi;
const string ServerIp = "192.0.2.1";
const int ServerPort = 28082;
const ulong PlayerId = 76561198000000000UL;
const int PlayerToken = -123456789;
using var rustPlus = new RustPlus(new RustPlusConnection(ServerIp, ServerPort, PlayerId, PlayerToken));
await rustPlus.ConnectAsync();
var response = await rustPlus.GetMapAsync();
if (!response.IsSuccess)
{
Console.Error.WriteLine($"GetMapAsync failed: {response.Error?.Message}");
return;
}
var map = response.Data!;
if (map.JpgImage is { Length: > 0 } jpg)
{
await File.WriteAllBytesAsync("map.jpg", jpg);
Console.WriteLine($"Saved map.jpg ({jpg.Length:N0} bytes, {map.Width}×{map.Height} game units)");
}
else
{
Console.Error.WriteLine("Server returned no map image.");
}
Note
ServerMap.JpgImage is byte[]? — check for null/empty before writing.
See RustPlus Client for the full GetMapAsync response shape.
3. Minimal team-chat echo bot
Subscribe to OnTeamChatReceived and echo back every message that is not your own. The guard
compares TeamMessage.SteamId against your own playerId so the bot does not echo itself into an
infinite loop.
using RustPlusApi;
using RustPlusApi.Data;
const string ServerIp = "192.0.2.1";
const int ServerPort = 28082;
const ulong PlayerId = 76561198000000000UL; // your SteamID64 — used as the echo guard
const int PlayerToken = -123456789;
using var rustPlus = new RustPlus(new RustPlusConnection(ServerIp, ServerPort, PlayerId, PlayerToken));
rustPlus.OnTeamChatReceived += async (_, msg) =>
{
// Ignore our own messages to avoid an infinite echo loop.
if (msg.SteamId == PlayerId)
return;
var echo = $"[bot] {msg.Name}: {msg.Message}";
Console.WriteLine($"Echo → {echo}");
var result = await rustPlus.SendTeamMessageAsync(echo);
if (!result.IsSuccess)
Console.Error.WriteLine($"SendTeamMessageAsync failed: {result.Error?.Message}");
};
await rustPlus.ConnectAsync();
Console.WriteLine("Echo bot running — press Ctrl+C to exit.");
await Task.Delay(Timeout.Infinite);
Note
SendTeamMessageAsync returns Response<TeamMessage?> — the payload is the server's echo of
your own message, not a confirmation. See RustPlus Client.
4. Camera snapshot loop
Subscribe to a camera, accumulate frames with CameraRenderer.AddRays, render a PNG, then
unsubscribe. Requires the RustPlusApi.Camera NuGet package (install with
dotnet add package RustPlusApi.Camera).
using RustPlusApi;
using RustPlusApi.Camera;
using RustPlusApi.Data.Cameras;
const string ServerIp = "192.0.2.1";
const int ServerPort = 28082;
const ulong PlayerId = 76561198000000000UL;
const int PlayerToken = -123456789;
const string CameraId = "CAM01"; // in-game identifier set on the computer station
const int FrameTarget = 10; // accumulate 10 frames before saving
using var rustPlus = new RustPlus(new RustPlusConnection(ServerIp, ServerPort, PlayerId, PlayerToken));
await rustPlus.ConnectAsync();
var sub = await rustPlus.SubscribeToCameraAsync(CameraId);
if (!sub.IsSuccess)
{
Console.Error.WriteLine($"Subscribe failed: {sub.Error?.Message}");
return;
}
var info = sub.Data!;
var renderer = new CameraRenderer(info.Width, info.Height);
var tcs = new TaskCompletionSource<bool>();
var frameCount = 0;
rustPlus.OnCameraRaysReceived += async (_, frame) =>
{
renderer.AddRays(frame);
if (++frameCount < FrameTarget)
return;
// Enough frames — render, save, unsubscribe.
var png = renderer.Render();
await File.WriteAllBytesAsync("snapshot.png", png);
Console.WriteLine($"Saved snapshot.png ({png.Length:N0} bytes) after {frameCount} frames.");
tcs.TrySetResult(true);
};
await tcs.Task; // wait until the snapshot is saved
await rustPlus.UnsubscribeFromCameraAsync();
Warning
Camera image fidelity is experimental — the decode has not yet been validated against a captured real frame. See Cameras for the rendering layer details.
5. Persist and reload credentials
Run the registration once, save to disk, and reload on subsequent runs. The one-await
PairingListener pattern handles the "pair in game and get the constructor args" step.
using RustPlusApi;
using RustPlusApi.Fcm.Registration;
const string ConfigPath = "rustplus.config.json";
// ── first run: acquire and persist FCM credentials ───────────────────────────
if (!File.Exists(ConfigPath))
{
Console.WriteLine("No credentials found — running registration flow (Chrome will open).");
var registration = new FcmRegistration();
var credentials = await registration.AcquireCredentialsAsync();
await registration.RegisterWithRustPlusAsync(credentials);
CredentialsStore.Save(ConfigPath, credentials);
Console.WriteLine($"Credentials saved to {ConfigPath}.");
}
// ── every run: load and listen for the next pairing ──────────────────────────
var creds = CredentialsStore.Load(ConfigPath);
Console.WriteLine("In game: open Rust+ → Pair with Server, then wait …");
using var pairingListener = new PairingListener(creds);
var pairing = await pairingListener.WaitForServerPairingAsync();
Console.WriteLine($"Paired: {pairing.Ip}:{pairing.Port} (player {pairing.PlayerId})");
// ── use the pairing values immediately ───────────────────────────────────────
using var rustPlus = new RustPlus(new RustPlusConnection(pairing.Ip, pairing.Port, pairing.PlayerId, pairing.PlayerToken));
await rustPlus.ConnectAsync();
var info = await rustPlus.GetInfoAsync();
if (info.IsSuccess)
Console.WriteLine($"{info.Data!.Name} — {info.Data.PlayerCount}/{info.Data.MaxPlayerCount} players");
Note
CredentialsStore.Save / Load write and read indented JSON (rustplus.config.json) in this
library's own format. (The legacy rustplus.js config layout is different — the
RustPlus.Fcm.ConsoleApp sample ships a loader that accepts both.)
See Credentials for the full registration flow, including browser discovery
order and upstream-fragility notes.