Skip to content
RustPluginGenerator

How to make a Rust kill feed plugin (CUI, last 5 kills)

Draws a CUI kill feed on the right side of every player's screen showing the last 5 player kills with killer, weapon and victim, using OnEntityDeath. Entries expire after a configurable number of seconds.

Rust has no kill feed for player-versus-player kills, so admins who want one have to build it themselves. Pulling the data is straightforward: the OnEntityDeath hook fires for every death, and its HitInfo tells you who caused it and with what. The hard part is presentation, because there is no built-in feed UI to feed into.

This plugin listens for deaths, keeps a short in-memory list of recent kill entries, and pushes a CUI panel to every connected player on the right edge of the screen. Each entry records the killer's display name, the weapon's display name and the victim's name, and gets a timestamp. A repeating timer drops expired entries and redraws the panels, so the feed shrinks on its own without any per-player state to clean up.

Everything the feed shows (how many entries, how long they live, panel size, colors, font size, a fallback weapon label, an enable switch) lives in the config, and every visible line goes through the language file so it can be translated.

The plugin code

KillFeedUI.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Game.Rust.Cui;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Kill Feed UI", "RustPluginGenerator", "1.0.0")]
    [Description("Shows a CUI kill feed with the last player kills on the right side of every player's screen.")]
    public class KillFeedUI : RustPlugin
    {
        private const string PermUse = "killfeedui.use";
        private const string PanelName = "killfeedui.panel";

        private class PluginConfig
        {
            [JsonProperty("Enabled")]
            public bool Enabled = true;
            [JsonProperty("Feed size")]
            public int FeedSize = 5;
            [JsonProperty("Entry seconds")]
            public int EntrySeconds = 10;
            [JsonProperty("Panel anchor min")]
            public string AnchorMin = "0.84 0.55";
            [JsonProperty("Panel anchor max")]
            public string AnchorMax = "0.99 0.90";
            [JsonProperty("Panel color")]
            public string PanelColor = "0.05 0.05 0.05 0.55";
            [JsonProperty("Text color")]
            public string TextColor = "1 1 1 1";
            [JsonProperty("Font size")]
            public int FontSize = 12;
            [JsonProperty("Fallback weapon name")]
            public string FallbackWeapon = "Unknown";
            [JsonProperty("Muted players")]
            public HashSet<string> MutedPlayers = new HashSet<string>();
        }

        private class KillEntry
        {
            public string Killer;
            public string Weapon;
            public string Victim;
            public float When;
        }

        private PluginConfig config;
        private readonly List<KillEntry> feed = new List<KillEntry>();
        private Timer expireTimer;

        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();
            }
            if (config.MutedPlayers == null) config.MutedPlayers = new HashSet<string>();
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["KillLine"] = "{0} [{1}] {2}",
                ["ToggledOff"] = "Kill feed hidden. Use /killfeed to show it again.",
                ["ToggledOn"] = "Kill feed shown.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, 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(PermUse, this);
        }

        private void OnServerInitialized()
        {
            if (!config.Enabled) return;
            expireTimer = timer.Every(1f, ExpireTick);
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected) continue;
                BuildFeed(player);
            }
        }

        private void Unload()
        {
            expireTimer?.Destroy();
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null) continue;
                CuiHelper.DestroyUi(player, PanelName);
            }
            SaveConfig();
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            if (!config.Enabled || entity == null || info == null) return;
            var victim = entity as BasePlayer;
            if (victim == null || victim.IsNpc) return;
            var killer = info.InitiatorPlayer;
            if (killer == null || killer.IsNpc || killer == victim) return;

            var weapon = info.Weapon?.GetItem()?.info?.displayName?.english ?? config.FallbackWeapon;
            feed.Add(new KillEntry
            {
                Killer = killer.displayName ?? "?",
                Weapon = weapon,
                Victim = victim.displayName ?? "?",
                When = UnityEngine.Time.realtimeSinceStartup
            });
            while (feed.Count > Mathf.Max(1, config.FeedSize)) feed.RemoveAt(0);
            RedrawAll();
        }

        private void ExpireTick()
        {
            if (!config.Enabled) return;
            float now = UnityEngine.Time.realtimeSinceStartup;
            if (feed.RemoveAll(e => now - e.When > config.EntrySeconds) > 0)
                RedrawAll();
        }

        private void RedrawAll()
        {
            foreach (var player in BasePlayer.activePlayerList)
            {
                if (player == null || !player.IsConnected) continue;
                BuildFeed(player);
            }
        }

        private void BuildFeed(BasePlayer player)
        {
            CuiHelper.DestroyUi(player, PanelName);
            if (!config.Enabled || config.MutedPlayers.Contains(player.UserIDString)) return;

            var container = new CuiElementContainer();
            var panel = container.Add(new CuiPanel
            {
                Image = { Color = config.PanelColor },
                RectTransform = { AnchorMin = config.AnchorMin, AnchorMax = config.AnchorMax },
                CursorEnabled = false
            }, "Overlay", PanelName);

            int count = feed.Count;
            int slots = Mathf.Max(1, config.FeedSize);
            for (int i = 0; i < count; i++)
            {
                var entry = feed[count - 1 - i];
                float top = 1f - i * (1f / slots);
                float bottom = top - (1f / slots);
                container.Add(new CuiLabel
                {
                    Text =
                    {
                        Text = Msg("KillLine", player.UserIDString, entry.Killer, entry.Weapon, entry.Victim),
                        FontSize = config.FontSize,
                        Color = config.TextColor,
                        Align = TextAnchor.MiddleLeft
                    },
                    RectTransform = { AnchorMin = $"0.03 {bottom:0.###}", AnchorMax = $"0.97 {top:0.###}" }
                }, panel);
            }
            CuiHelper.AddUi(player, container);
        }

        private void OnPlayerSleepEnded(BasePlayer player)
        {
            if (player == null || !config.Enabled) return;
            BuildFeed(player);
        }

        [ChatCommand("killfeed")]
        private void CmdKillFeed(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            if (!config.MutedPlayers.Remove(player.UserIDString))
            {
                config.MutedPlayers.Add(player.UserIDString);
                CuiHelper.DestroyUi(player, PanelName);
                player.ChatMessage(Msg("ToggledOff", player.UserIDString));
            }
            else
            {
                BuildFeed(player);
                player.ChatMessage(Msg("ToggledOn", player.UserIDString));
            }
            SaveConfig();
        }
    }
}

Hooks used: OnServerInitialized, OnEntityDeath, OnPlayerSleepEnded

Config file

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

KillFeedUI.json
{
  "Enabled": true,
  "Feed size": 5,
  "Entry seconds": 10,
  "Panel anchor min": "0.84 0.55",
  "Panel anchor max": "0.99 0.90",
  "Panel color": "0.05 0.05 0.05 0.55",
  "Text color": "1 1 1 1",
  "Font size": 12,
  "Fallback weapon name": "Unknown",
  "Muted players": []
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/killfeedToggles the kill feed panel on or off for the calling player.killfeedui.use
Permissions
PermissionGrants
killfeedui.useAllows the player to use /killfeed to hide or show their own feed panel.

How the code works

  1. Capture deaths in OnEntityDeath

    The hook signature is private void OnEntityDeath(BaseCombatEntity entity, HitInfo info). Resource entities also pass through this hook, so the code first checks entity is BasePlayer (via an as cast) and bails on a null info. From there info.InitiatorPlayer gives the killer. If it is null, the victim is an NPC, or the killer equals the victim, the death is skipped. Only player-versus-player deaths reach the feed.

  2. Resolve killer, victim and weapon names

    The victim's name comes from victim.displayName. The killer's name comes from killer.displayName, where killer is the info.InitiatorPlayer cast to BasePlayer. The weapon name is read with info.Weapon?.GetItem()?.info?.displayName?.english; if any link in that chain is null the configurable Fallback weapon name is used instead. That covers turrets, traps and unarmed kills.

  3. Keep a bounded entry list

    Each kill becomes a KillEntry holding Killer, Weapon, Victim and a When timestamp taken from UnityEngine.Time.realtimeSinceStartup. Entries are appended to the feed list, and a while loop removes from the front until the count fits config.FeedSize (default 5). The list is memory only; there is no file write for it, since a kill feed has no meaning after a restart.

  4. Render the CUI panel

    BuildFeed(BasePlayer player) destroys the previous killfeedui.panel and, if the plugin is disabled or the player is muted, stops there. Otherwise it builds a CuiElementContainer with one CuiPanel using AnchorMin and AnchorMax from config, then one CuiLabel per entry inside it. Labels are laid out from the top down, newest first, and each line is built by Msg("KillLine", ...) so the format lives in the language file.

  5. Expire entries with a timer

    A single timer started in OnServerInitialized with timer.Every(1f, ExpireTick) checks each entry's age against config.EntrySeconds. When entries fall off, RedrawAll() runs, which loops BasePlayer.activePlayerList and calls BuildFeed for each. The timer reference is stored in expireTimer and destroyed in Unload, so one shared timer drives every panel and nothing is left running.

  6. Player toggle and lifecycle cleanup

    /killfeed (permission killfeedui.use) toggles the player's id inside config.MutedPlayers, a HashSet<string> stored in the plugin config. Muted players are skipped in BuildFeed and their panel is destroyed immediately. OnPlayerSleepEnded rebuilds the panel when a player wakes. Unload destroys the timer, removes every player's panel and saves the config so the mute list survives a restart.

Install it on your server

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

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

Variations

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

  • Team-colored kill feed

    Colors killer and victim names by team relationship so you can see friend-or-foe at a glance.

    Generate this version
  • Console kill logging

    Prints every player kill to the server console so it appears in RCON logs.

    Generate this version
  • Headshot marker

    Adds a marker to feed lines when the kill was a headshot.

    Generate this version

FAQ

Why does the feed ignore NPC and animal deaths?

The hook requires the dying entity to be a BasePlayer that is not an NPC, and info.InitiatorPlayer to exist. So scientists and animals killed by players are skipped, and environmental deaths (fall, cold, decay) have no player initiator and are skipped too.

Does the feed survive a plugin reload?

No. On unload every panel is destroyed and the in-memory kill list is thrown away, so the feed starts empty after a reload. The per-player mute setting does survive, because Muted players lives in the plugin config file and is reloaded with it.

How much does the feed cost on a full server?

One timer fires every second but only redraws when an entry expired or a kill arrives. A redraw destroys and re-adds a small container per connected viewer, roughly a handful of CUI elements each. If your server runs other heavy CUI, lower Feed size, raise Entry seconds, or let players hide the panel with /killfeed.

How do I install it on Oxide or Carbon?

Drop KillFeedUI.cs into oxide/plugins on Oxide or carbon/plugins on Carbon, then reload the plugin or restart the server. Edit oxide/config/KillFeedUI.json (Carbon uses carbon/configs) and reload to apply. Grant killfeedui.use to the default group with oxide.grant group default killfeedui.use if you want everyone to have the toggle.

Can the feed be hidden per player?

Yes, with /killfeed for anyone holding killfeedui.use. The choice is stored in the Muted players array of oxide/config/KillFeedUI.json, so it persists across restarts and only affects that player's own panel.

Customize it for your server

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

Prompt: Build a Rust Oxide plugin that shows a CUI kill feed on the right of every player's screen from OnEntityDeath, keeping the last 5 player kills with killer, weapon and victim, entries expiring after 10 seconds, with a toggle command.
Customize for my server

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

Last updated Oct 26, 2026.

Customize kill feed

Customize