Skip to content
RustPluginGenerator

How to make a Rust team death alerts plugin (killer and grid)

Sends a chat alert to every online teammate when a player dies, including the killer name and the map grid of the death. Uses OnPlayerDeath and RelationshipManager team lookup.

When a teammate dies while you are across the map, the vanilla game gives you almost nothing: no killer name, no location, just a status change in the team list. Admins often want a proper alert so groups actually know what happened and can react.

This plugin hooks OnPlayerDeath, resolves the victim's team through RelationshipManager.ServerInstance.FindTeam, and sends a formatted chat line to each member who is online and not the victim. The killer is read from info.Initiator: a player gets their display name, common NPCs get a readable label, and other entities fall back to their short prefab name. The grid is derived from World.Size, the configured cell size and transform.position with a small helper, so no external map plugin is needed.

Everything is configurable: alert format, whether to include the grid, whether to skip suicide or environment deaths, and a cooldown so a team is not spammed when several members die at once during a raid.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Team Death Alerts", "RustPluginGenerator", "1.0.0")]
    [Description("Sends online teammates a chat alert with the killer name and map grid when a player dies.")]
    public class TeamDeathAlerts : RustPlugin
    {
        private const string PermUse = "teamdeathalerts.use";
        private const string PermAdmin = "teamdeathalerts.admin";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> lastAlert = new Dictionary<ulong, float>();

        private class PluginConfig
        {
            [JsonProperty("Alert format (body text)")]
            public string AlertFormat { get; set; } = "<color=#ff6b6b>{victim}</color> was killed by <color=#ffd166>{killer}</color> at grid <color=#8ecae6>{grid}</color> ({damage}).";

            [JsonProperty("Show grid in alerts")]
            public bool ShowGrid { get; set; } = true;

            [JsonProperty("Grid cell size (metres)")]
            public float GridSize { get; set; } = 146f;

            [JsonProperty("Ignore suicide deaths")]
            public bool IgnoreSuicide { get; set; } = true;

            [JsonProperty("Ignore deaths with no attacker")]
            public bool IgnoreEnvironment { get; set; } = false;

            [JsonProperty("Alert cooldown per team (seconds)")]
            public float AlertCooldown { get; set; } = 3f;
        }

        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>
            {
                ["Alert"] = "{0}",
                ["UnknownKiller"] = "an unknown source",
                ["DamageFormat"] = "{0} damage",
                ["TestPrefix"] = "[TeamDeathAlerts] Sample alert:",
                ["StatusHeader"] = "TeamDeathAlerts status:",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

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

        private string Msg(string key, string userId = null, params object[] args)
            => string.Format(lang.GetMessage(key, this, userId), args);

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

            var damageType = info?.damageTypes != null
                ? info.damageTypes.GetMajorityDamageType()
                : Rust.DamageType.Generic;

            if (config.IgnoreSuicide && damageType == Rust.DamageType.Suicide) return null;

            BaseEntity attacker = info?.Initiator;
            if (config.IgnoreEnvironment && attacker == null) return null;

            var team = RelationshipManager.ServerInstance.FindTeam(player.currentTeam);
            if (team == null || team.members == null || team.members.Count < 2) return null;

            float now = Time.realtimeSinceStartup;
            float last;
            if (config.AlertCooldown > 0f && lastAlert.TryGetValue(player.currentTeam, out last) && now - last < config.AlertCooldown)
                return null;
            lastAlert[player.currentTeam] = now;

            string killer = ResolveKillerName(attacker);
            string grid = config.ShowGrid ? GetGrid(player.transform.position) : "";
            string damage = Msg("DamageFormat", null, PrettyDamage(damageType));

            string body = config.AlertFormat
                .Replace("{victim}", player.displayName)
                .Replace("{killer}", killer)
                .Replace("{grid}", grid)
                .Replace("{damage}", damage);

            foreach (ulong memberId in team.members)
            {
                if (memberId == player.userID.Get()) continue;
                BasePlayer member = BasePlayer.FindByID(memberId);
                if (member == null || !member.IsConnected || member.IsSleeping()) continue;
                member.ChatMessage(Msg("Alert", member.UserIDString, body));
            }

            return null;
        }

        private string ResolveKillerName(BaseEntity attacker)
        {
            if (attacker == null) return Msg("UnknownKiller");

            var attackerPlayer = attacker as BasePlayer;
            if (attackerPlayer != null)
                return attackerPlayer.displayName;

            var scientist = attacker as ScientistNPC;
            if (scientist != null)
            {
                if (scientist.ShortPrefabName != null && scientist.ShortPrefabName.Contains("scarecrow"))
                    return "Scarecrow";
                return "Scientist";
            }

            if (attacker is BaseAnimalNPC) return "Animal";
            if (attacker is PatrolHelicopter) return "Patrol Helicopter";
            if (attacker is BradleyAPC) return "Bradley APC";

            return attacker.ShortPrefabName ?? Msg("UnknownKiller");
        }

        private string PrettyDamage(Rust.DamageType type)
        {
            switch (type)
            {
                case Rust.DamageType.Bullet: return "bullet";
                case Rust.DamageType.Explosion: return "explosion";
                case Rust.DamageType.Slash: return "melee";
                case Rust.DamageType.Stab: return "melee";
                case Rust.DamageType.Blunt: return "melee";
                case Rust.DamageType.Heat: return "fire";
                case Rust.DamageType.Fall: return "fall";
                case Rust.DamageType.Suicide: return "suicide";
                case Rust.DamageType.Decay: return "decay";
                case Rust.DamageType.Bite: return "animal";
                default: return type.ToString().ToLowerInvariant();
            }
        }

        private string GetGrid(Vector3 pos)
        {
            float cell = config.GridSize <= 0f ? 146f : config.GridSize;
            int column = Mathf.FloorToInt((pos.x + World.Size / 2f) / cell);
            int row = Mathf.FloorToInt((pos.z + World.Size / 2f) / cell);
            char letter = (char)('A' + Mathf.Clamp(column, 0, 25));
            return string.Concat(letter, row.ToString());
        }

        [ChatCommand("tda")]
        private void CmdTda(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length == 0 || args[0].ToLowerInvariant() != "test")
            {
                player.ChatMessage("/tda test");
                return;
            }

            string body = config.AlertFormat
                .Replace("{victim}", player.displayName)
                .Replace("{killer}", "Example Killer")
                .Replace("{grid}", config.ShowGrid ? GetGrid(player.transform.position) : "")
                .Replace("{damage}", Msg("DamageFormat", null, "bullet"));

            player.ChatMessage(Msg("TestPrefix", player.UserIDString));
            player.ChatMessage(Msg("Alert", player.UserIDString, body));
        }

        [ConsoleCommand("teamdeathalerts.status")]
        private void CcmdStatus(ConsoleSystem.Arg arg)
        {
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            string text = Msg("StatusHeader") +
                          "\nAlert format: " + config.AlertFormat +
                          "\nShow grid: " + config.ShowGrid +
                          "\nGrid size: " + config.GridSize +
                          "\nIgnore suicide: " + config.IgnoreSuicide +
                          "\nIgnore environment: " + config.IgnoreEnvironment +
                          "\nCooldown: " + config.AlertCooldown;

            if (player != null) player.ChatMessage(text);
            else arg.ReplyWith(text);
        }
    }
}

Hooks used: OnPlayerDeath

Config file

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

TeamDeathAlerts.json
{
  "Alert format (body text)": "<color=#ff6b6b>{victim}</color> was killed by <color=#ffd166>{killer}</color> at grid <color=#8ecae6>{grid}</color> ({damage}).",
  "Show grid in alerts": true,
  "Grid cell size (metres)": 146,
  "Ignore suicide deaths": true,
  "Ignore deaths with no attacker": false,
  "Alert cooldown per team (seconds)": 3
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/tda testSends you a sample death alert to preview the configured format.teamdeathalerts.use
teamdeathalerts.statusPrints the current config values to the caller or the server console.teamdeathalerts.admin
Permissions
PermissionGrants
teamdeathalerts.useAllows the player to run /tda test to preview an alert.
teamdeathalerts.adminAllows running the teamdeathalerts.status console command from the server console, RCON or in game.

How the code works

  1. Reading the hook and filtering deaths

    OnPlayerDeath(BasePlayer player, HitInfo info) fires for every death. The method returns early for null players, NPCs and players without a team (currentTeam == 0). It reads info.damageTypes.GetMajorityDamageType() and skips the death when IgnoreSuicide is true and the type is Rust.DamageType.Suicide. When IgnoreEnvironment is on it also skips deaths where info.Initiator is null. Returning null keeps vanilla behavior because this hook is only used for observation.

  2. Resolving the team and its online members

    The plugin calls RelationshipManager.ServerInstance.FindTeam(player.currentTeam) and bails out if the team is null or has fewer than two members. It then iterates team.members, resolving each ulong id with BasePlayer.FindByID, which returns null for offline players. IsConnected is checked and IsSleeping() is skipped, and the dead player is excluded by comparing memberId with player.userID.Get() so nobody is messaged about their own death.

  3. Naming the killer

    ResolveKillerName takes info.Initiator. A BasePlayer attacker contributes displayName. A ScientistNPC is labeled Scarecrow or Scientist depending on whether ShortPrefabName contains scarecrow, a BaseAnimalNPC is Animal, and PatrolHelicopter and BradleyAPC get their own labels. Anything else falls back to ShortPrefabName, and a null attacker returns the UnknownKiller lang entry. The majority damage type is converted to a short word by PrettyDamage and placed in the {damage} placeholder.

  4. Computing the grid

    Rust maps are square and centered, so the column is Mathf.FloorToInt((pos.x + World.Size / 2f) / cell) and the row the same on z, where cell comes from the GridSize config key and defaults to 146. The column is clamped to 0-25 and turned into a letter with (char)('A' + column). When ShowGrid is false the {grid} placeholder is replaced with an empty string instead.

  5. Formatting and sending the alert

    The AlertFormat string from the config holds placeholders {victim}, {killer}, {grid} and {damage}. The plugin builds the body once, then sends it to each qualifying teammate with member.ChatMessage using the Alert lang key, which is a {0} wrapper around the body. Wrapper, prefix and unknown-killer strings come from LoadDefaultMessages in oxide/lang/en/TeamDeathAlerts.json; the body itself is edited in the config.

  6. Cooldown and commands

    A Dictionary<ulong, float> keyed by team id stores the last alert time from Time.realtimeSinceStartup. When AlertCooldown is above zero and the new death is inside the window, the alert is suppressed so a wipe does not flood the chat. /tda test is checked against teamdeathalerts.use and renders the format with sample values; teamdeathalerts.status is checked against teamdeathalerts.admin and prints each config value.

Install it on your server

  1. Save TeamDeathAlerts.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/TeamDeathAlerts.json (Carbon: carbon/configs/TeamDeathAlerts.json). After editing, reload it:oxide.reload TeamDeathAlerts c.reload TeamDeathAlerts # Carbon
  4. Grant the permission

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

Variations

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

  • Team chat only

    Sends the alert to the team's chat channel instead of a direct message to each member.

    Generate this version
  • Position on the map

    Replaces the grid text with a map marker on the death location.

    Generate this version
  • Kill feed to all players

    Broadcasts every PvP kill to the whole server, not just the victim's team.

    Generate this version

FAQ

Does this alert players who are offline or sleeping?

No. BasePlayer.FindByID only returns a player object for connected clients, and the plugin additionally checks IsConnected and skips sleeping players. Offline teammates see nothing; a data-file queue could be added if you want them notified after reconnect.

Are NPC and animal kills reported?

Yes by default. When info.Initiator is a scientist, animal, helicopter or Bradley the alert uses the matching label from ResolveKillerName. Set IgnoreEnvironment to true to skip deaths where info.Initiator is null, and IgnoreSuicide to hide suicide deaths.

Why do I get no alert on some deaths?

The most common causes are AlertCooldown suppressing a second death in the same team within the configured window, the victim having no team (currentTeam is 0), or a plugin such as a PvE damage blocker preventing the kill from reaching OnPlayerDeath.

How do I install it?

Drop TeamDeathAlerts.cs into oxide/plugins, let Oxide compile it, then edit oxide/config/TeamDeathAlerts.json and run oxide.reload TeamDeathAlerts. Carbon loads the same file from its plugins folder and supports the same config and lang layout.

How do I change the wording?

Edit oxide/lang/en/TeamDeathAlerts.json for the wrapper line and AlertFormat in the config for the body. Placeholders are plain replacements, so any mix of {victim}, {killer}, {grid} and {damage} works, including repeating a placeholder.

Customize it for your server

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

Prompt: Create a Rust plugin that alerts online teammates when a player dies, showing victim, killer, damage type and map grid, with configurable alert format, cooldown and ignore options.
Customize for my server

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

Last updated Oct 26, 2026.

Customize team death alerts

Customize