Skip to content
RustPluginGenerator

How to make a Rust death cause stats plugin (Oxide/Carbon)

This plugin records every player death, classifies the cause from the HitInfo passed to OnPlayerDeath, and stores running totals per player so /deaths can print a breakdown of PvP kills, NPCs, animals, falls, drowning, cold and other causes.

Admins often want to know how their players actually die: are people falling off bases, freezing to death, or getting farmed by NPCs and wildlife? Vanilla Rust does not keep that history per player, so you need a small plugin that listens to deaths and buckets them. This recipe uses the OnPlayerDeath hook, inspects the incoming HitInfo (attacker player, initiating entity, majority damage type) and sorts each death into a fixed set of causes. Counts are saved to oxide/data so they survive restarts, and players can run /deaths to see their own totals, ordering the categories from most to least. Admins with the permission can pass a name or Steam ID to look at another player. No external services, no database, just one data file and one chat command.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Death Cause Stats", "RustPluginGenerator", "1.0.0")]
    [Description("Records each player's deaths per cause and shows the breakdown on demand.")]
    public class DeathCauseStats : RustPlugin
    {
        private const string PermAdmin = "deathcausestats.admin";

        private PluginConfig config;
        private StoredData data;
        private Timer saveTimer;

        private class PluginConfig
        {
            [JsonProperty("Save interval in seconds")]
            public int SaveIntervalSeconds = 300;

            [JsonProperty("Broadcast deaths to all players")]
            public bool BroadcastDeaths = false;

            [JsonProperty("Max causes shown in chat")]
            public int MaxCausesShown = 8;
        }

        private class StoredData
        {
            [JsonProperty("Players")]
            public Dictionary<string, Dictionary<string, int>> Players = new Dictionary<string, Dictionary<string, int>>();
        }

        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>
            {
                ["Header"] = "<color=#ce9e39>Death breakdown for {0}</color>",
                ["Footer"] = "Total deaths: {0}",
                ["Line"] = "{0}: {1}",
                ["Truncated"] = "and {0} more cause(s) not shown",
                ["NoDeaths"] = "{0} has not died yet.",
                ["Self"] = "You",
                ["Broadcast"] = "{0} died ({1}).",
                ["Invalid"] = "That player could not be found.",
                ["Denied"] = "You do not have permission to look up other players.",
                ["Cause.player"] = "Killed by player",
                ["Cause.npc"] = "Killed by NPC/turret",
                ["Cause.animal"] = "Killed by animal",
                ["Cause.self"] = "Suicide",
                ["Cause.fall"] = "Fall damage",
                ["Cause.drown"] = "Drowning",
                ["Cause.cold"] = "Cold",
                ["Cause.radiation"] = "Radiation",
                ["Cause.bleed"] = "Bleeding out",
                ["Cause.explosion"] = "Explosion",
                ["Cause.decay"] = "Decay",
                ["Cause.other"] = "Other"
            }, 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(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
            saveTimer = timer.Every(Math.Max(30, config.SaveIntervalSeconds), SaveData);
        }

        private void Unload()
        {
            if (saveTimer != null)
            {
                saveTimer.Destroy();
                saveTimer = null;
            }
            SaveData();
        }

        private void SaveData()
        {
            if (data != null)
                Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        private object OnPlayerDeath(BasePlayer player, HitInfo info)
        {
            if (player == null || data == null || player.IsNpc)
                return null;

            string cause = Classify(player, info);
            string id = player.UserIDString;

            Dictionary<string, int> causes;
            if (!data.Players.TryGetValue(id, out causes))
            {
                causes = new Dictionary<string, int>();
                data.Players[id] = causes;
            }

            int current;
            causes.TryGetValue(cause, out current);
            causes[cause] = current + 1;

            if (config.BroadcastDeaths)
                Server.Broadcast(Msg("Broadcast", null, player.displayName, Msg("Cause." + cause)));

            return null;
        }

        private string Classify(BasePlayer victim, HitInfo info)
        {
            if (info == null)
                return "self";

            BasePlayer attacker = info.InitiatorPlayer;
            if (attacker != null && !attacker.IsNpc)
                return attacker == victim ? "self" : "player";

            BaseEntity initiator = info.Initiator;
            if (initiator != null)
            {
                if (initiator == victim)
                    return "self";
                if (initiator is BaseAnimalNPC)
                    return "animal";
                if (initiator is ScientistNPC || initiator is BradleyAPC || initiator is BaseHelicopter)
                    return "npc";
            }

            Rust.DamageType type = info.damageTypes.GetMajorityDamageType();
            switch (type)
            {
                case Rust.DamageType.Fall: return "fall";
                case Rust.DamageType.Cold: return "cold";
                case Rust.DamageType.Radiation: return "radiation";
                case Rust.DamageType.Bleeding: return "bleed";
                case Rust.DamageType.Explosion: return "explosion";
                case Rust.DamageType.Decay: return "decay";
                case Rust.DamageType.Suicide: return "self";
                case Rust.DamageType.Bullet: return attacker != null ? "npc" : "other";
                default: return "other";
            }
        }

        [ChatCommand("deaths")]
        private void CmdDeaths(BasePlayer player, string command, string[] args)
        {
            if (player == null)
                return;

            string targetId = player.UserIDString;
            string targetName = player.displayName;
            bool isSelf = true;

            if (args != null && args.Length > 0)
            {
                if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
                {
                    SendReply(player, Msg("Denied", player.UserIDString));
                    return;
                }

                BasePlayer target = BasePlayer.FindAwakeOrSleeping(args[0]);
                if (target == null)
                {
                    SendReply(player, Msg("Invalid", player.UserIDString));
                    return;
                }

                targetId = target.UserIDString;
                targetName = target.displayName;
                isSelf = target == player;
            }

            Dictionary<string, int> causes;
            if (!data.Players.TryGetValue(targetId, out causes) || causes.Count == 0)
            {
                SendReply(player, Msg("NoDeaths", player.UserIDString, isSelf ? Msg("Self", player.UserIDString) : targetName));
                return;
            }

            SendReply(player, Msg("Header", player.UserIDString, targetName));

            int total = causes.Values.Sum();
            var ordered = causes.OrderByDescending(kv => kv.Value).ToList();
            int shown = 0;

            foreach (var entry in ordered)
            {
                if (shown >= config.MaxCausesShown)
                    break;
                SendReply(player, Msg("Line", player.UserIDString, Msg("Cause." + entry.Key), entry.Value));
                shown++;
            }

            if (ordered.Count > shown)
                SendReply(player, Msg("Truncated", player.UserIDString, ordered.Count - shown));

            SendReply(player, Msg("Footer", player.UserIDString, total));
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerDeath

Config file

Created automatically on first load at oxide/config/DeathCauseStats.json.

DeathCauseStats.json
{
  "Save interval in seconds": 300,
  "Broadcast deaths to all players": false,
  "Max causes shown in chat": 8
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/deaths [name or steamid]Shows the caller's death breakdown. With a name or Steam ID argument (requires the permission) it shows that player's totals instead.deathcausestats.admin
Permissions
PermissionGrants
deathcausestats.adminAllows /deaths with a name or Steam ID argument to look up another player's death breakdown; without it, /deaths with an argument is refused.

How the code works

  1. Capture the death and its HitInfo

    OnPlayerDeath(BasePlayer player, HitInfo info) fires after the player's health reaches zero. The hook returns an object but you return null to leave default behavior alone. Null-check player first, ignore player.IsNpc, and treat a null info as self. Everything else is derived from info.InitiatorPlayer, info.Initiator and info.damageTypes.GetMajorityDamageType().

  2. Classify the cause into fixed buckets

    The private Classify method returns one of the cause keys: player, npc, animal, self, fall, cold, radiation, bleed, explosion, decay or other. A non-NPC InitiatorPlayer wins first (the victim means self, anyone else means player). Otherwise the Initiator entity decides: BaseAnimalNPC is animal, ScientistNPC, BradleyAPC and BaseHelicopter are npc. With no attacker, the majority Rust.DamageType maps the rest; drowning has no branch and falls through to other.

  3. Store per-player counters

    A nested StoredData class holds a Dictionary<string, Dictionary<string, int>> named Players, keyed by Steam ID, so each player has their own cause-to-count dictionary. It is loaded in OnServerInitialized with Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) and written back by SaveData, which runs from a timer.Every using the Save interval in seconds config value (clamped to at least 30 seconds) and again in Unload.

  4. Serve the breakdown through lang messages

    /deaths is a [ChatCommand("deaths")]. When an argument is present it checks the deathcausestats.admin permission, resolves the target with BasePlayer.FindAwakeOrSleeping, and reads that player's dictionary. Causes are ordered with OrderByDescending by count, then rendered with the Line lang key. Every cause label is its own lang key (Cause.fall, Cause.npc, ...) so names are translatable, and totals go through Header and Footer.

  5. Keep the chat output size bounded

    A chat message is limited, so the list stops at Max causes shown in chat (default 8). When more categories exist, the remaining count is summarised in a single Truncated line. If the player has no recorded deaths, the NoDeaths message is sent instead of the header.

  6. Optional broadcast and persistence

    When Broadcast deaths to all players is true, OnPlayerDeath calls Server.Broadcast with the Broadcast lang key, the victim's name and the cause label. The save timer is kept in a Timer field and Destroy()ed in Unload, where SaveData runs once more. Data lives in oxide/data/DeathCauseStats.json, which admins can delete between wipes to reset the counters.

Install it on your server

  1. Save DeathCauseStats.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/DeathCauseStats.json (Carbon: carbon/configs/DeathCauseStats.json). After editing, reload it:oxide.reload DeathCauseStats c.reload DeathCauseStats # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default deathcausestats.admin c.grant group default deathcausestats.admin # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Server-wide aggregation

    Adds a second data set that totals every death by cause across the whole server and prints it with a console command.

    Generate this version
  • Kill/death ratio

    Tracks kills separately from deaths so players see their K/D next to the breakdown.

    Generate this version
  • Top deaths leaderboard

    A chat command lists the players with the most deaths in each cause over the current save.

    Generate this version

FAQ

How do I install it on Oxide or Carbon?

Drop DeathCauseStats.cs into oxide/plugins, reload the server or run oxide.reload DeathCauseStats. Carbon reads the same file from its plugins folder. A default config is generated in oxide/config; the data file in oxide/data is written by the save timer or when the plugin unloads.

Does it count NPC or animal deaths?

No. The OnPlayerDeath hook is filtered with player.IsNpc so scientist corpses and other server-spawned NPCs are skipped. Player deaths caused by NPCs, animals, Bradley or patrol helicopter fire are still counted under the npc and animal causes.

Why does a fall sometimes show as `other`?

Rust reports the majority damage type of the final hit, and a fall that ends with a slash or bullet can log a different cause. The plugin only uses the majority type when no valid player, NPC or animal attacker is found, so mixed deaths fall back to whatever the game reports.

Where is the data stored and can I reset it?

Counts live in oxide/data/DeathCauseStats.json, keyed by Steam ID. Stop the server, delete or replace the file, and start again to clear all statistics. The config controls how often the file is written back, which defaults to 300 seconds.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Write an Oxide plugin called DeathCauseStats that counts player deaths per cause (player, npc, animal, self, fall, drown, cold, radiation, bleed, explosion) via OnPlayerDeath, stores totals per Steam ID in oxide/data, and serves /deaths with an admin lookup argument.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize death cause stats

Customize