FCM Notifications
RustPlusFcm (in RustPlusApi.Fcm) connects to Firebase Cloud Messaging and raises events when
Rust+ sends push notifications — pairing requests and alarm triggers.
How notifications reach your app
flowchart LR
A[Player pairs device<br/>in-game] --> B[Facepunch servers]
B --> C[FCM / mtalk.google.com]
C --> D[RustPlusFcm]
D --> E["OnServerPairing<br/>OnEntityPairing<br/>OnAlarmTriggered<br/>…"]
Connect
using RustPlusApi.Fcm;
using RustPlusApi.Fcm.Registration;
var credentials = CredentialsStore.Load("rustplus.config.json");
var listener = new RustPlusFcm(credentials, persistentIds: null);
await listener.ConnectAsync();
// …
listener.Disconnect();
persistentIds is an optional ICollection<string> of already-seen notification IDs. These are
server-assigned ids that the socket replays to the FCM server at login so it won't redeliver
messages you've already processed. Prefer a HashSet<string> — the collection is consulted on
every message and grows over time, so a List<string>'s linear scan degrades over long sessions.
The socket adds each newly-harvested id to your collection and then raises PersistentIdReceived
with that id — subscribe to persist ids incrementally and avoid a redelivery window after a restart
(not just a reconnect). Read the full tracked set at any time via PersistentIds, which returns a
never-null snapshot. The caller's set is not cleared on login; you own its lifecycle, including
pruning (ids have a server-side lifespan).
Events
Every typed event payload derives NotificationBase, which carries ServerId (the originating
Rust+ server) and PersistentId (the FCM message id — correlate with the PersistentIdReceived
harvested set to de-duplicate or audit). Notification<T> adds PlayerId/PlayerToken/Data
for pairing events; AlarmNotification adds Title/Message for alarm events.
| Event | Payload type | Fires when |
|---|---|---|
OnPairing |
FcmMessage |
Any pairing FCM message is received (raw). |
OnEntityPairing |
Notification<EntityEvent?> |
You pair a smart device (superset of the three below). |
OnSmartSwitchPairing |
Notification<ulong?> |
A smart switch is paired (entity ID in Data). |
OnSmartAlarmPairing |
Notification<ulong?> |
A smart alarm is paired (entity ID in Data). |
OnStorageMonitorPairing |
Notification<ulong?> |
A storage monitor is paired (entity ID in Data). |
OnServerPairing |
Notification<ServerEvent?> |
You choose Pair with Server in game — carries ip/port/playerId/playerToken. |
OnAlarmTriggered |
AlarmNotification? |
A paired smart alarm fires. |
Socket lifecycle events (from IRustPlusFcmSocket):
| Event | Payload type | Fires when |
|---|---|---|
Connecting |
EventArgs |
ConnectAsync is called, before TLS handshake. |
Connected |
EventArgs |
TLS handshake and MCS login completed. |
NotificationReceived |
string |
A raw FCM notification JSON string is received. |
SocketClosed |
EventArgs |
The server sent a close tag. |
Disconnecting |
EventArgs |
Disconnect is called. |
Disconnected |
EventArgs |
The receive loop has stopped. |
ErrorOccurred |
Exception |
An unhandled error occurred on the receive loop. |
listener.OnServerPairing += (_, e) =>
Console.WriteLine($"Pair: {e.Data?.Ip}:{e.Data?.Port} (player {e.PlayerId})");
listener.OnAlarmTriggered += (_, alarm) =>
Console.WriteLine($"Alarm: {alarm?.Title}");
Note
The listener sends its own MCS heartbeat ping every 5 minutes (to keep NAT/firewall
mappings alive) and watches for inactivity: if no frame arrives for 12 minutes, the
connection is presumed dead — ErrorOccurred fires with a TimeoutException and the socket
disconnects so you can create a fresh listener. Both intervals are tunable via
RustPlusFcmSocketOptions.
var listener = new RustPlusFcm(credentials, options: new RustPlusFcmSocketOptions
{
HeartbeatInterval = TimeSpan.FromMinutes(2),
InactivityTimeout = TimeSpan.FromMinutes(6)
});
Reconnect strategy
RustPlusFcm is single-connection: after Disconnect() or disposal you must create a new
instance. The pattern below handles ErrorOccurred (including the TimeoutException raised by the
inactivity watchdog) with exponential back-off, and passes the same persistentIds collection so
notifications already processed are not replayed.
var credentials = CredentialsStore.Load("rustplus.config.json");
// Persist this collection across reconnects to deduplicate replayed notifications.
ICollection<string> persistentIds = new HashSet<string>();
RustPlusFcm? listener = null;
var delay = TimeSpan.FromSeconds(5);
const int MaxDelaySeconds = 300;
async Task ConnectWithRetryAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
listener?.Disconnect();
listener?.Dispose();
listener = new RustPlusFcm(credentials, persistentIds);
listener.OnAlarmTriggered += (_, alarm) =>
Console.WriteLine($"Alarm: {alarm?.Title}");
listener.ErrorOccurred += async (_, ex) =>
{
Console.WriteLine($"FCM error ({ex.GetType().Name}): {ex.Message}");
// Back off then reconnect.
await Task.Delay(delay, ct);
delay = TimeSpan.FromSeconds(Math.Min(delay.TotalSeconds * 2, MaxDelaySeconds));
_ = ConnectWithRetryAsync(ct);
};
try
{
await listener.ConnectAsync(ct);
delay = TimeSpan.FromSeconds(5); // reset back-off on success
break;
}
catch (Exception ex) when (ex is not OperationCanceledException)
{
Console.WriteLine($"Connect failed: {ex.Message}");
await Task.Delay(delay, ct);
delay = TimeSpan.FromSeconds(Math.Min(delay.TotalSeconds * 2, MaxDelaySeconds));
}
}
}
await ConnectWithRetryAsync(CancellationToken.None);
Persisting across restarts
The reconnect loop above keeps ids in memory only — a process restart loses them and the server may
replay recently-delivered messages. To survive restarts, load the set from disk on startup and save
it incrementally via PersistentIdReceived:
const string IdsFile = "persistent-ids.json";
// Load on startup.
ICollection<string> persistentIds = File.Exists(IdsFile)
? JsonSerializer.Deserialize<HashSet<string>>(File.ReadAllText(IdsFile)) ?? new HashSet<string>()
: new HashSet<string>();
// Wire up before ConnectAsync so no id is missed.
listener.PersistentIdReceived += (_, _) =>
File.WriteAllText(IdsFile, JsonSerializer.Serialize(listener.PersistentIds));
For a real app, prune the stored set periodically — FCM persistent ids have a server-side lifespan and the set grows without bound otherwise.
One-await pairing
For the common "wait for the next server pairing" case, RustPlusApi.Fcm.Registration provides
PairingListener, which wraps RustPlusFcm and returns a strongly-typed ServerPairing:
using var pairing = new PairingListener(credentials);
ServerPairing server = await pairing.WaitForServerPairingAsync();
using var rustPlus = new RustPlus(new RustPlusConnection(server.Ip, server.Port, server.PlayerId, server.PlayerToken));
See Credentials for how to obtain the FCM credentials.