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
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.
{
"Save interval in seconds": 300,
"Broadcast deaths to all players": false,
"Max causes shown in chat": 8
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/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 |
| Permission | Grants |
|---|---|
deathcausestats.admin | Allows /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
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 returnnullto leave default behavior alone. Null-checkplayerfirst, ignoreplayer.IsNpc, and treat a nullinfoasself. Everything else is derived frominfo.InitiatorPlayer,info.Initiatorandinfo.damageTypes.GetMajorityDamageType().Classify the cause into fixed buckets
The private
Classifymethod returns one of the cause keys:player,npc,animal,self,fall,cold,radiation,bleed,explosion,decayorother. A non-NPCInitiatorPlayerwins first (the victim meansself, anyone else meansplayer). Otherwise theInitiatorentity decides:BaseAnimalNPCisanimal,ScientistNPC,BradleyAPCandBaseHelicopterarenpc. With no attacker, the majorityRust.DamageTypemaps the rest; drowning has no branch and falls through toother.Store per-player counters
A nested
StoredDataclass holds aDictionary<string, Dictionary<string, int>>namedPlayers, keyed by Steam ID, so each player has their own cause-to-count dictionary. It is loaded inOnServerInitializedwithInterface.Oxide.DataFileSystem.ReadObject<StoredData>(Name)and written back bySaveData, which runs from atimer.Everyusing theSave interval in secondsconfig value (clamped to at least 30 seconds) and again inUnload.Serve the breakdown through lang messages
/deathsis a[ChatCommand("deaths")]. When an argument is present it checks thedeathcausestats.adminpermission, resolves the target withBasePlayer.FindAwakeOrSleeping, and reads that player's dictionary. Causes are ordered withOrderByDescendingby count, then rendered with theLinelang key. Every cause label is its own lang key (Cause.fall,Cause.npc, ...) so names are translatable, and totals go throughHeaderandFooter.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 singleTruncatedline. If the player has no recorded deaths, theNoDeathsmessage is sent instead of the header.Optional broadcast and persistence
When
Broadcast deaths to all playersis true,OnPlayerDeathcallsServer.Broadcastwith theBroadcastlang key, the victim's name and the cause label. The save timer is kept in aTimerfield andDestroy()ed inUnload, whereSaveDataruns once more. Data lives inoxide/data/DeathCauseStats.json, which admins can delete between wipes to reset the counters.
Install it on your server
Save DeathCauseStats.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/DeathCauseStats.json(Carbon:carbon/configs/DeathCauseStats.json). After editing, reload it:oxide.reload DeathCauseStats c.reload DeathCauseStats # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionKill/death ratio
Tracks kills separately from deaths so players see their K/D next to the breakdown.
Generate this versionTop 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More stats plugins
- Gather statisticsTracks wood, stone, metal ore, sulfur ore and cloth gathered per player for the current wipe, persists the totals in oxide/data, and shows them withโฆOnServerInitializedOnDispenserGatheredOnCollectiblePickup
- Last seen commandRecords a timestamp for every player on disconnect and answers /seen <name> with a relative time such as '2 hours ago' or 'online now'.OnServerInitializedOnPlayerConnectedOnPlayerDisconnected
- Longest kill shotTracks each player's longest projectile kill distance and the server-wide record, saved to oxide/data, with a /longest chat command and anโฆOnServerInitializedOnPlayerDeath
Last updated Oct 26, 2026.
Customize death cause stats
Customize