Skip to content
RustPluginGenerator

OnPlayerDisconnected

OnPlayerDisconnected is called by ServerMgr.OnDisconnected just before Rust tears down the disconnecting player, giving plugins a last chance to read that player's state.

Signature
void OnPlayerDisconnected(BasePlayer player, string reason)

Parameters

Hook parameters
ParameterTypeDescription
playerBasePlayerThe BasePlayer whose connection is being closed. Still valid in this call: net identity, inventory, position and team are all readable, but the player is already leaving and will be destroyed shortly after.
reasonstringThe disconnect reason string passed to ServerMgr.OnDisconnected, typically something like a timeout, kick or network error message. It can be empty or a generic string for clean client-side quits.

Return value

The return value is ignored: the hook is called for its side effects only, and the original method proceeds to basePlayer.OnDisconnected() and the rest of the logout sequence regardless of what you return. Do not return a value expecting to cancel the disconnect; there is no way to keep a player from leaving through this hook.

How it works

This hook fires from ServerMgr.OnDisconnected after EAC has processed the leave and after the connection's player has been resolved, but before basePlayer.OnDisconnected() runs. That means the player object is still intact when your code executes: you can read position, inventory contents, team id, health and permissions, then record or act on them. Because it sits inside the disconnection path, it fires for every kind of departure the server processes: clean quits, timeouts, kicks and network drops all funnel through this method. The reason argument is whatever string the server passed into the disconnect routine and should be treated as informational only, not as a reliable enum. Declaring the first parameter as BasePlayer is the natural choice; narrowing to a subclass is pointless here since real players are never NPCs. Anything you do in this hook must be fast, because the original code continues to run right after it returns. Persist data now, do not schedule work that depends on the player still being connected.

Example plugin: Record last known position on disconnect

DisconnectLogger.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("DisconnectLogger", "RustPluginGenerator", "1.0.0")]
    [Description("Records the last known position and reason for every player disconnect")]
    public class DisconnectLogger : RustPlugin
    {
        private const string PermView = "disconnectlogger.view";
        private const string DataFile = "DisconnectLogger";

        private StoredData data;

        private class PluginConfig
        {
            [JsonProperty("Log to console")]
            public bool LogToConsole { get; set; } = true;

            [JsonProperty("Keep at most this many records per player")]
            public int MaxRecordsPerPlayer { get; set; } = 5;
        }

        private class Record
        {
            public string Name;
            public string Reason;
            public string Position;
            public string Timestamp;
        }

        private class StoredData
        {
            public Dictionary<string, List<Record>> Records = new Dictionary<string, List<Record>>();
        }

        private PluginConfig config;

        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);

        private void Init()
        {
            permission.RegisterPermission(PermView, this);
        }

        private void Load()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataFile) ?? new StoredData();
            if (data.Records == null) data.Records = new Dictionary<string, List<Record>>();
        }

        private void Unload()
        {
            SaveData();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null || player.net == null) return;

            string id = player.UserIDString;
            if (string.IsNullOrEmpty(id)) return;

            var record = new Record
            {
                Name = player.displayName ?? "unknown",
                Reason = string.IsNullOrEmpty(reason) ? "none" : reason,
                Position = player.transform.position.ToString(),
                Timestamp = DateTime.UtcNow.ToString("u")
            };

            if (!data.Records.TryGetValue(id, out List<Record> list))
            {
                list = new List<Record>();
                data.Records[id] = list;
            }

            list.Add(record);
            int max = Mathf.Max(1, config.MaxRecordsPerPlayer);
            while (list.Count > max) list.RemoveAt(0);

            SaveData();

            if (config.LogToConsole)
            {
                Puts($"{record.Name} ({id}) disconnected at {record.Position}. Reason: {record.Reason}");
            }
        }

        [ChatCommand("dcinfo")]
        private void CmdDcInfo(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermView))
            {
                player.ChatMessage("You do not have permission to use this command.");
                return;
            }

            if (args == null || args.Length == 0)
            {
                player.ChatMessage("Usage: /dcinfo <steamId>");
                return;
            }

            if (!data.Records.TryGetValue(args[0], out List<Record> list) || list.Count == 0)
            {
                player.ChatMessage("No disconnect records for that Steam ID.");
                return;
            }

            foreach (Record record in list)
            {
                player.ChatMessage($"{record.Timestamp} {record.Name}: {record.Position} ({record.Reason})");
            }
        }
    }
}

The plugin keeps a small data file in oxide/data/DisconnectLogger keyed by Steam ID, storing the display name, disconnect reason, position and timestamp each time a player drops. OnPlayerDisconnected null-checks the player and its net identity, builds a record from live values, and writes it through Interface.Oxide.DataFileSystem. An optional chat command, gated by the disconnectlogger.admin permission, prints the stored entry for a given Steam ID so administrators can check where and when a player left. The data object is loaded in Load and saved in Unload, keeping disk writes out of the disconnect path except for the final WriteObject call, which is cheap for a flat dictionary.

Common use cases

  • Freeze combat logouts

    Compare the player's stored login position or last damage timestamp with their position at disconnect and log suspicious teleports to a file for staff review.

  • Persist per-player session stats

    Flush accumulated playtime, kills or gathered resources for the session into your data file at disconnect so nothing is lost when the player never returns.

  • Clean up external state

    Remove the player from custom vote timers, raid alerts, spectate lists or party bookkeeping that would otherwise hold a stale reference.

Pitfalls

  • Cannot block the disconnect

    The hook's return value is discarded and the original logout path always continues, so bans, kicks or custom rejection logic must run elsewhere, such as an earlier authentication hook.

  • Do not assume reason is meaningful

    The string comes straight from the disconnect call and varies by cause; parse it for display only and never drive permission or punishment logic from its contents.

  • Keep the hook fast

    Heavy work such as scanning all entities or sending network messages delays the disconnection routine. Persist a minimal record and defer any cleanup to a timer.

Plugin recipes that use OnPlayerDisconnected

Build a plugin with OnPlayerDisconnected

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Freeze combat logouts using the OnPlayerDisconnected hook, with a config file and a permission
Generate this plugin

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 OnPlayerDisconnected

Generate