OnUserDisconnected
OnUserDisconnected is called when a connected user leaves the server, right after Rust's disconnect handling runs and before the Covalance player manager drops the player. It passes the Oxide IPlayer and the raw disconnect reason.
void OnUserDisconnected(IPlayer player, string reason)Parameters
| Parameter | Type | Description |
|---|---|---|
player | IPlayer | The IPlayer (Covalence player object) for the user who disconnected. Use player.Id to get the Steam ID string and player.Name for the display name; cast to BasePlayer when the player object is still alive. |
reason | string | The disconnect reason string supplied by the game, e.g. a kick message, a network timeout, or an empty string for a normal quit. It is whatever the server passed into OnPlayerDisconnected, so treat it as informational only. |
Return value
The hook is void and its return value is ignored; the injection calls it and then continues into Covalance.PlayerManager.PlayerDisconnected. Returning nothing changes nothing, and there is no way to cancel the disconnection from this hook.
How it works
This hook fires from RustCore.OnPlayerDisconnected, which the server calls for every client that is removed from the session: clean quits, timeouts, kicks and bans all end up here. The game resolves the player's BasePlayer, reads its overlapping IPlayer (Covalence) object, and only then calls Interface.CallHook("OnUserDisconnected", player, reason). Because the hook fires inside that path, the player still exists as a network entity when your plugin runs, but it is the last reliable moment to read it.
The first argument is an IPlayer, not a BasePlayer. Declaring a more specific type is not useful here because Oxide passes the Covalance object, so keep the parameter as IPlayer and call player.Object as BasePlayer when you need game members such as inventory or currentTeam. The reason string is free-form and often empty; do not branch on exact wording.
Use this hook to tear down per-player state: saved preferences, open UI, timers keyed on the user, cached loot sessions. It fires once per disconnection, unlike OnPlayerDisconnected which does not exist in Oxide, so it is the canonical place to clean up.
Example plugin: Wipe per-player playtime state on disconnect
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Libraries.Covalence;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Disconnect Playtime Tracker", "RustPluginGenerator", "1.0.0")]
[Description("Tracks session playtime and persists totals when a user disconnects.")]
public class DisconnectPlaytimeTracker : RustPlugin
{
private const string PermView = "disconnectplaytimetracker.view";
private PluginConfig config;
private StoredData data;
private readonly Dictionary<ulong, float> sessionStart = new Dictionary<ulong, float>();
private Timer flushTimer;
private class PluginConfig
{
[JsonProperty("Flush interval seconds")]
public float FlushInterval { get; set; } = 120f;
[JsonProperty("Show session time on disconnect")]
public bool ShowSessionTime { get; set; } = true;
}
private class StoredData
{
[JsonProperty("Totals")]
public Dictionary<ulong, double> Totals { get; set; } = new Dictionary<ulong, double>();
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try { config = Config.ReadObject<PluginConfig>(); if (config == null) throw new Exception(); }
catch { PrintWarning("Configuration file is invalid, using defaults"); LoadDefaultConfig(); }
SaveConfig();
}
protected override void SaveConfig() => Config.WriteObject(config);
protected override void LoadDefaultMessages()
{
lang.RegisterMessages(new Dictionary<string, string>
{
["Total"] = "Total playtime: {0}",
["NoData"] = "No playtime recorded for you yet.",
["Session"] = "You left after {0}."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermView, this);
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
flushTimer = timer.Every(config.FlushInterval, Flush);
}
private void Unload()
{
Flush();
if (flushTimer != null) flushTimer.Destroy();
}
private void OnPlayerConnected(BasePlayer player)
{
if (player == null) return;
sessionStart[player.userID.Get()] = Time.realtimeSinceStartup;
}
private void OnUserDisconnected(IPlayer player, string reason)
{
if (player == null) return;
if (!ulong.TryParse(player.Id, out ulong userId)) return;
float start;
if (sessionStart.TryGetValue(userId, out start))
{
AddSeconds(userId, Time.realtimeSinceStartup - start);
sessionStart.Remove(userId);
}
if (config.ShowSessionTime && player.IsConnected == false)
{
// Informational only; the player already left, so this is logged.
Puts(string.Format("{0} ({1}) disconnected: {2}", player.Name, player.Id, string.IsNullOrEmpty(reason) ? "no reason" : reason));
}
}
private void AddSeconds(ulong userId, float seconds)
{
if (seconds <= 0f) return;
double existing;
data.Totals.TryGetValue(userId, out existing);
data.Totals[userId] = existing + seconds;
}
private void Flush()
{
if (data == null) return;
Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
[ChatCommand("playtime")]
private void CmdPlaytime(BasePlayer player, string command, string[] args)
{
if (player == null) return;
double total;
if (!data.Totals.TryGetValue(player.userID.Get(), out total))
{
SendReply(player, Msg("NoData", player.UserIDString));
return;
}
SendReply(player, Msg("Total", player.UserIDString, FormatSeconds(total)));
}
private string FormatSeconds(double seconds)
{
TimeSpan span = TimeSpan.FromSeconds(seconds);
return string.Format("{0}h {1}m", (int)span.TotalHours, span.Minutes);
}
}
}The plugin records how long each user has been connected, using OnPlayerConnected to stamp a start time and this hook to add the elapsed seconds to a persisted dictionary keyed by Steam ID. OnUserDisconnected resolves the IPlayer.Object to a BasePlayer to read userID reliably, then removes the open session entry to avoid leaks. A periodic timer flushes the totals through Interface.Oxide.DataFileSystem. The /playtime chat command reads the stored total and replies through the language messages, so admins can verify that disconnects are being captured without waiting for a save cycle.
Common use cases
Clean up per-player timers and UI
Destroy any CUI elements and timers you registered for a user when they leave, so a later reconnect starts with a clean slate. The player object still exists at this point, which makes CuiHelper.DestroyUi safe to call.
Persist session statistics
Flush accumulated playtime, kills during the session, or economy balances to oxide/data when the player disconnects instead of only on server save. This reduces data loss on hard crashes.
Log departures with reason
Write the display name, Steam ID and reason string to a data file or broadcast to admins so they can distinguish clean quits from timeouts and kicks.
Pitfalls
IPlayer is not a BasePlayer
Declaring the parameter as BasePlayer makes Oxide skip the call entirely because the argument is an IPlayer. Keep the type as IPlayer and cast player.Object as BasePlayer when you need game members.
The reason string is not standardised
Do not match on exact reason text to detect bans or kicks; it varies by Facepunch and other plugins. Use OnPlayerBanned or OnPlayerKicked for those cases and treat reason as informational.
Do not assume the entity is fully gone
This hook runs before Covalance.PlayerManager.PlayerDisconnected, so the player's BasePlayer is still around. Anything that depends on the entity being removed should run on OnPlayerSleepEnd or a later OnPlayerDisconnected-style hook instead.
Build a plugin with OnUserDisconnected
Describe what should happen and get a compiled plugin that uses this hook.
Edit the prompt on the next screen. 2 free plugins a month, no card.
Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.
Build a plugin with OnUserDisconnected
Generate