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 to skip on
reconnect. Pass the same collection instance across reconnects — the socket appends every newly-seen
ID to it as they arrive, so passing it back to a new instance automatically deduplicates replays.
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<int?> |
A smart switch is paired (entity ID in Data). |
OnSmartAlarmPairing |
Notification<int?> |
A smart alarm is paired (entity ID in Data). |
OnStorageMonitorPairing |
Notification<int?> |
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 |
AlarmEvent? |
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 List<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);
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.