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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/killfeed | Toggles the kill feed panel on or off for the calling player. | killfeedui.use |
| Permission | Grants |
|---|---|
killfeedui.use | Allows the player to use /killfeed to hide or show their own feed panel. |
How the code works
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 checksentity is BasePlayer(via anascast) and bails on a nullinfo. From thereinfo.InitiatorPlayergives 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.Resolve killer, victim and weapon names
The victim's name comes from
victim.displayName. The killer's name comes fromkiller.displayName, wherekilleris theinfo.InitiatorPlayercast toBasePlayer. The weapon name is read withinfo.Weapon?.GetItem()?.info?.displayName?.english; if any link in that chain is null the configurableFallback weapon nameis used instead. That covers turrets, traps and unarmed kills.Keep a bounded entry list
Each kill becomes a
KillEntryholdingKiller,Weapon,Victimand aWhentimestamp taken fromUnityEngine.Time.realtimeSinceStartup. Entries are appended to thefeedlist, and awhileloop removes from the front until the count fitsconfig.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.Render the CUI panel
BuildFeed(BasePlayer player)destroys the previouskillfeedui.paneland, if the plugin is disabled or the player is muted, stops there. Otherwise it builds aCuiElementContainerwith oneCuiPanelusingAnchorMinandAnchorMaxfrom config, then oneCuiLabelper entry inside it. Labels are laid out from the top down, newest first, and each line is built byMsg("KillLine", ...)so the format lives in the language file.Expire entries with a timer
A single timer started in
OnServerInitializedwithtimer.Every(1f, ExpireTick)checks each entry's age againstconfig.EntrySeconds. When entries fall off,RedrawAll()runs, which loopsBasePlayer.activePlayerListand callsBuildFeedfor each. The timer reference is stored inexpireTimerand destroyed inUnload, so one shared timer drives every panel and nothing is left running.Player toggle and lifecycle cleanup
/killfeed(permissionkillfeedui.use) toggles the player's id insideconfig.MutedPlayers, aHashSet<string>stored in the plugin config. Muted players are skipped inBuildFeedand their panel is destroyed immediately.OnPlayerSleepEndedrebuilds the panel when a player wakes.Unloaddestroys the timer, removes every player's panel and saves the config so the mute list survives a restart.
Install it on your server
Save KillFeedUI.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/KillFeedUI.json(Carbon:carbon/configs/KillFeedUI.json). After editing, reload it:oxide.reload KillFeedUI c.reload KillFeedUI # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionConsole kill logging
Prints every player kill to the server console so it appears in RCON logs.
Generate this versionHeadshot 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More ui plugins
- Admin menuBuilds a CUI admin panel that opens with /am and renders one button per action defined in the config, each button running a console command throughโฆ
- Announcement bannerAdds an /announce chat command that draws a CUI banner at the top of every connected player's screen for a configurable duration, plus timedโฆOnServerInitializedOnPlayerConnected
- Command shortcut menuAdds a /menu chat command that opens a CUI grid of configurable buttons; each button closes the panel and runs its configured chat command for thatโฆ
Last updated Oct 26, 2026.
Customize kill feed
Customize