Skip to content
RustPluginGenerator

How to make a Rust kill streak plugin (Oxide/Carbon)

Tracks consecutive player kills per user, announces configurable milestones in global chat and grants item rewards, then resets the streak when the player dies or disconnects. Uses OnEntityDeath and OnPlayerDisconnected.

Kill streak plugins give PvP a progression layer that vanilla Rust does not track: players want to know who is currently on a run and what the next milestone is worth. The naive version stores the count only in memory, which breaks when the server restarts or when a player quits and rejoins, and it often counts NPC kills or environmental deaths as PvP kills.

This plugin keeps a per-userID dictionary of streak counts and saves it to oxide/data so a restart does not wipe anyone's run. OnEntityDeath checks that the victim is a real, connected player and that the attacker is a different player, then increments the attacker, writes the new value and fires any milestone whose threshold was just reached. Milestones are defined in the config as a list of kill counts, reward shortnames and amounts. Dying or disconnecting removes the player from the dictionary entirely, so the next kill starts at one.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("KillStreaks", "RustPluginGenerator", "1.0.0")]
    [Description("Tracks consecutive PvP kills and rewards configurable milestones.")]
    public class KillStreaks : RustPlugin
    {
        private const string PermUse = "killstreaks.use";
        private const string PermAdmin = "killstreaks.admin";
        private const string DataFile = "KillStreaks";

        private class Milestone
        {
            [JsonProperty("Kills required")]
            public int Kills { get; set; }

            [JsonProperty("Reward shortname")]
            public string RewardShortname { get; set; }

            [JsonProperty("Reward amount")]
            public int RewardAmount { get; set; }
        }

        private class PluginConfig
        {
            [JsonProperty("Announce every kill")]
            public bool AnnounceEveryKill { get; set; }

            [JsonProperty("Minimum streak to announce a lost streak")]
            public int MinimumStreakToAnnounceLoss { get; set; }

            [JsonProperty("Milestones")]
            public List<Milestone> Milestones { get; set; }
        }

        private PluginConfig config;
        private Dictionary<string, int> streaks = new Dictionary<string, int>();

        protected override void LoadDefaultConfig()
        {
            config = new PluginConfig
            {
                AnnounceEveryKill = false,
                MinimumStreakToAnnounceLoss = 5,
                Milestones = new List<Milestone>
                {
                    new Milestone { Kills = 5, RewardShortname = "scrap", RewardAmount = 50 },
                    new Milestone { Kills = 10, RewardShortname = "supply.signal", RewardAmount = 1 },
                    new Milestone { Kills = 15, RewardShortname = "rifle.ak", RewardAmount = 1 }
                }
            };
        }

        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>
            {
                ["Milestone"] = "{0} reached a {1} kill streak!",
                ["EveryKill"] = "{0} is on a {1} kill streak.",
                ["StreakLost"] = "{0} lost a {1} kill streak.",
                ["YourStreak"] = "Your current kill streak: {0}",
                ["NoStreak"] = "You have no active kill streak.",
                ["TopHeader"] = "Top kill streaks:",
                ["TopEntry"] = "{0}. {1} - {2}",
                ["TopEmpty"] = "Nobody is on a kill streak right now.",
                ["ResetDone"] = "Cleared the streak for {0}.",
                ["ResetNotFound"] = "No streak found for {0}.",
                ["ResetUsage"] = "Usage: /streak reset <name or steamid>",
                ["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);
            permission.RegisterPermission(PermAdmin, this);
        }

        private void OnServerInitialized()
        {
            var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>(DataFile);
            if (loaded != null) streaks = loaded;
        }

        private void Unload()
        {
            SaveData();
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, streaks);
        }

        private void OnEntityDeath(BasePlayer victim, HitInfo info)
        {
            if (victim == null || victim.IsNpc) return;

            string victimId = victim.UserIDString;
            int lostStreak = GetStreak(victimId);
            if (lostStreak > 0)
            {
                streaks.Remove(victimId);
                SaveData();
                if (lostStreak >= config.MinimumStreakToAnnounceLoss)
                    Server.Broadcast(Msg("StreakLost", null, victim.displayName, lostStreak));
            }

            if (info == null) return;
            BasePlayer attacker = info.InitiatorPlayer;
            if (attacker == null || attacker.IsNpc || attacker == victim || !attacker.IsConnected) return;
            if (attacker.userID == victim.userID) return;

            string attackerId = attacker.UserIDString;
            int newStreak = GetStreak(attackerId) + 1;
            streaks[attackerId] = newStreak;
            SaveData();

            Milestone milestone = FindMilestone(newStreak);
            if (milestone != null)
            {
                Server.Broadcast(Msg("Milestone", null, attacker.displayName, newStreak));
                GiveReward(attacker, milestone);
            }
            else if (config.AnnounceEveryKill)
            {
                Server.Broadcast(Msg("EveryKill", null, attacker.displayName, newStreak));
            }
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            string userId = player.UserIDString;
            if (streaks.Remove(userId)) SaveData();
        }

        private int GetStreak(string userId)
        {
            int value;
            return streaks.TryGetValue(userId, out value) ? value : 0;
        }

        private Milestone FindMilestone(int kills)
        {
            if (config.Milestones == null) return null;
            foreach (var milestone in config.Milestones)
            {
                if (milestone != null && milestone.Kills == kills) return milestone;
            }
            return null;
        }

        private void GiveReward(BasePlayer player, Milestone milestone)
        {
            if (string.IsNullOrEmpty(milestone.RewardShortname) || milestone.RewardAmount <= 0) return;
            Item item = ItemManager.CreateByName(milestone.RewardShortname, milestone.RewardAmount);
            if (item == null) return;
            player.GiveItem(item);
        }

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

            if (args != null && args.Length > 0 && args[0].ToLower() == "top")
            {
                ShowTop(player);
                return;
            }

            if (args != null && args.Length > 0 && args[0].ToLower() == "reset")
            {
                HandleReset(player, args);
                return;
            }

            int streak = GetStreak(player.UserIDString);
            SendReply(player, streak > 0 ? Msg("YourStreak", player.UserIDString, streak) : Msg("NoStreak", player.UserIDString));
        }

        private void ShowTop(BasePlayer player)
        {
            var list = new List<KeyValuePair<string, int>>(streaks);
            list.Sort((a, b) => b.Value.CompareTo(a.Value));
            if (list.Count == 0)
            {
                SendReply(player, Msg("TopEmpty", player.UserIDString));
                return;
            }

            SendReply(player, Msg("TopHeader", player.UserIDString));
            int shown = 0;
            foreach (var entry in list)
            {
                if (shown >= 5) break;
                string name = GetDisplayName(entry.Key);
                SendReply(player, Msg("TopEntry", player.UserIDString, shown + 1, name, entry.Value));
                shown++;
            }
        }

        private string GetDisplayName(string userId)
        {
            BasePlayer found = BasePlayer.FindAwakeOrSleeping(userId);
            return found != null ? found.displayName : userId;
        }

        private void HandleReset(BasePlayer player, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args.Length < 2)
            {
                SendReply(player, Msg("ResetUsage", player.UserIDString));
                return;
            }

            string target = args[1];
            string targetId = target;
            BasePlayer found = BasePlayer.FindAwakeOrSleeping(target);
            if (found != null) targetId = found.UserIDString;

            if (streaks.Remove(targetId))
            {
                SaveData();
                SendReply(player, Msg("ResetDone", player.UserIDString, target));
            }
            else
            {
                SendReply(player, Msg("ResetNotFound", player.UserIDString, target));
            }
        }
    }
}

Hooks used: OnServerInitialized, OnEntityDeath, OnPlayerDisconnected

Config file

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

KillStreaks.json
{
  "Announce every kill": false,
  "Minimum streak to announce a lost streak": 5,
  "Milestones": [
    {
      "Kills required": 5,
      "Reward shortname": "scrap",
      "Reward amount": 50
    },
    {
      "Kills required": 10,
      "Reward shortname": "supply.signal",
      "Reward amount": 1
    },
    {
      "Kills required": 15,
      "Reward shortname": "rifle.ak",
      "Reward amount": 1
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/streakShows your current kill streak.killstreaks.use
/streak topLists the highest current kill streaks on the server.killstreaks.use
/streak reset <name or steamid>Clears a player's streak.killstreaks.admin
Permissions
PermissionGrants
killstreaks.useAllows the /streak and /streak top commands.
killstreaks.adminAllows /streak reset to clear another player's streak.

How the code works

  1. Count only real PvP kills

    OnEntityDeath(BaseCombatEntity entity, HitInfo info) fires for every entity death, including animals, scientists and structures. The plugin declares the parameter as BasePlayer instead of BaseCombatEntity, so Oxide's hook dispatch only calls it when the dead entity is a player; NPC deaths never reach the method. Inside it checks that the player is not an NPC and that info.InitiatorPlayer exists, is connected and has a different userID from the victim.

  2. Store streaks per userID

    Counts live in a Dictionary<string, int> keyed by player.UserIDString, loaded in OnServerInitialized through Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, int>>(DataFile) and written back in Unload and after every change. Loading inside OnServerInitialized rather than Init means the data file system is already available, and it keeps a restart from dropping a streak.

  3. Fire milestones from config

    The config holds a Milestones list of objects with Kills, RewardShortname and RewardAmount. After incrementing a streak the plugin looks for a milestone whose Kills equals the new count and runs the announce-and-reward logic only then, so a player passing 5 does not re-trigger the 5 reward on kill 6. Unknown shortnames are caught because ItemManager.CreateByName returns null for them.

  4. Announce to the server

    Milestones use Server.Broadcast with text pulled from the language file, formatted with the player name and kill count. Ordinary kills stay silent by default (the AnnounceEveryKill config option turns them on) because broadcasting every kill on a busy server floods chat. Milestone broadcasts are rare enough that they never spam.

  5. Reset on death and disconnect

    Death is handled inside the same OnEntityDeath method: if the dead player had a streak of at least MinimumStreakToAnnounceLoss, the server is told it ended. The entry is removed either way. OnPlayerDisconnected(BasePlayer player, string reason) does the same removal, which stops players from preserving a streak by leaving the server when they are about to die.

  6. Give the reward safely

    Rewards are built with ItemManager.CreateByName(config.RewardShortname, config.RewardAmount) and handed over with player.GiveItem(item), which drops the item at the player's feet if their inventory is full. The reward shortname and amount come from the matched milestone, so each threshold can give something different, for example 10 kills for a supply signal and 25 for an M249.

Install it on your server

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

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

Variations

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

FAQ

Do NPC or animal kills count?

No. The hook parameter is declared as BasePlayer, so Oxide only calls the method when the dead entity is a player, and the attacker must also be a connected player. Scientist and animal deaths are ignored.

What happens to streaks on server restart?

They survive. The dictionary is written to oxide/data/KillStreaks.json on every change and loaded again in OnServerInitialized. A restart does not count as a death, so an active streak continues after the server comes back.

Why does quitting reset the streak?

OnPlayerDisconnected removes the player's entry. Without it, a player losing a fight could disconnect to keep the streak for their next session. Admins who want streaks to survive relogs can delete that removal line.

How do I change the milestone thresholds?

Edit oxide/config/KillStreaks.json. The Milestones array holds objects with Kills, RewardShortname and RewardAmount. Add, remove or reorder entries freely, then run oxide.reload KillStreaks to apply them.

How do I install it on Oxide or Carbon?

Drop the .cs file into oxide/plugins, where Carbon also reads it, and the plugin compiles on the next reload. The config file is created automatically on first load and permissions are registered when the plugin starts.

Customize it for your server

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

Prompt: Write a Rust Oxide kill streak plugin that tracks consecutive PvP kills, saves them to oxide/data, announces configurable milestones at 5, 10, 15 and 25 kills in global chat and gives each one an item reward, resetting on death and disconnect.
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 streaks

Customize