Skip to content
RustPluginGenerator

How to make a Rust first blood plugin (announce + reward first kill)

Announces the first player kill after a restart as First Blood and pays the killer a configured reward. It listens to OnEntityDeath, stores the claim time in oxide/data so it survives restarts, and optionally wipes that data on a new save via OnNewSave.

Servers with a lot of PvP traffic rarely notice the first kill of the day, and the killer gets nothing but a corpse. A first blood notification gives the early hours of a wipe some structure: one kill is special, everyone sees it, and the killer walks away with something for it.

This plugin hooks OnEntityDeath, filters down to player-versus-player kills by checking that both the victim and the InitiatorPlayer on the HitInfo are real connected players, then checks a persisted flag. Only the first valid kill after a restart is announced. The reward is a set of items defined in config, given with ItemManager.CreateByName and player.GiveItem, plus an optional chat and console broadcast.

The claim is written to oxide/data/FirstBlood.json so a restart does not hand out a second reward, and OnNewSave resets the flag when a wipe deletes the map data.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("First Blood", "RustPluginGenerator", "1.0.0")]
    [Description("Announces the first player kill after a restart and rewards the killer.")]
    public class FirstBlood : RustPlugin
    {
        private const string PermAdmin = "firstblood.admin";
        private const string PermReset = "firstblood.reset";
        private const string DataFile = "FirstBlood";

        private PluginConfig config;
        private FirstBloodData data;

        private class PluginConfig
        {
            [JsonProperty("Announce First Blood")]
            public bool Announce { get; set; } = true;

            [JsonProperty("Reward Enabled")]
            public bool RewardEnabled { get; set; } = true;

            [JsonProperty("Reward Items")]
            public List<RewardItem> RewardItems { get; set; } = new List<RewardItem>
            {
                new RewardItem { Shortname = "scrap", Amount = 250, Skin = 0 },
                new RewardItem { Shortname = "supply.signal", Amount = 1, Skin = 0 }
            };

            [JsonProperty("Claimed")]
            public bool Claimed { get; set; } = false;

            [JsonProperty("Killer Id")]
            public string KillerId { get; set; } = string.Empty;

            [JsonProperty("Killer Name")]
            public string KillerName { get; set; } = string.Empty;

            [JsonProperty("Timestamp")]
            public int Timestamp { get; set; } = 0;
        }

        private class RewardItem
        {
            [JsonProperty("Shortname")]
            public string Shortname { get; set; } = "scrap";

            [JsonProperty("Amount")]
            public int Amount { get; set; } = 1;

            [JsonProperty("Skin")]
            public ulong Skin { get; set; } = 0;
        }

        private class FirstBloodData
        {
            [JsonProperty("Claimed")]
            public bool Claimed { get; set; } = false;

            [JsonProperty("Killer Id")]
            public string KillerId { get; set; } = string.Empty;

            [JsonProperty("Killer Name")]
            public string KillerName { get; set; } = string.Empty;

            [JsonProperty("Timestamp")]
            public int Timestamp { get; set; } = 0;
        }

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

        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>
            {
                ["FirstBlood.Broadcast"] = "FIRST BLOOD: {0} killed {1} with {2} ({3}m)",
                ["FirstBlood.Reward"] = "You received the first blood reward: {0}",
                ["FirstBlood.Status.Claimed"] = "First blood claimed by {0} at {1}.",
                ["FirstBlood.Status.Open"] = "First blood is still open.",
                ["FirstBlood.Reset"] = "First blood claim cleared.",
                ["FirstBlood.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 OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<FirstBloodData>(DataFile) ?? new FirstBloodData();
            if (data.Claimed)
                Puts($"First blood already claimed by {data.KillerName} ({data.KillerId}).");
            else
                Puts("First blood is open for the next PvP kill.");
        }

        private void OnNewSave(string filename)
        {
            data = new FirstBloodData();
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
        }

        private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
        {
            if (data == null || data.Claimed || info == null) return;

            BasePlayer victim = entity as BasePlayer;
            if (victim == null || victim.IsNpc || !victim.IsConnected) return;

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

            data.Claimed = true;
            data.KillerId = killer.UserIDString;
            data.KillerName = killer.displayName;
            data.Timestamp = Facepunch.Math.Epoch.Current;

            config.Claimed = true;
            config.KillerId = killer.UserIDString;
            config.KillerName = killer.displayName;
            config.Timestamp = data.Timestamp;
            SaveConfig();

            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);

            string weapon = info.WeaponPrefab != null ? info.WeaponPrefab.ShortPrefabName : "unknown";
            int distance = Mathf.RoundToInt(info.ProjectileDistance);

            if (config.Announce)
                Server.Broadcast(Msg("FirstBlood.Broadcast", null, killer.displayName, victim.displayName, weapon, distance));

            if (!config.RewardEnabled) return;

            List<string> given = new List<string>();
            foreach (RewardItem entry in config.RewardItems)
            {
                Item item = ItemManager.CreateByName(entry.Shortname, entry.Amount, entry.Skin);
                if (item == null) continue;
                if (!killer.inventory.GiveItem(item))
                    item.Drop(killer.transform.position, Vector3.zero);
                given.Add($"{entry.Amount}x {item.info.displayName.english}");
            }

            if (given.Count > 0)
                killer.ChatMessage(Msg("FirstBlood.Reward", killer.UserIDString, string.Join(", ", given)));
        }

        [ChatCommand("firstblood")]
        private void CmdFirstBlood(BasePlayer player, string command, string[] args)
        {
            if (player == null || args == null || args.Length == 0) return;

            switch (args[0].ToLower())
            {
                case "status":
                    if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
                    {
                        player.ChatMessage(Msg("FirstBlood.NoPermission", player.UserIDString));
                        return;
                    }
                    if (data == null || !data.Claimed)
                    {
                        player.ChatMessage(Msg("FirstBlood.Status.Open", player.UserIDString));
                        return;
                    }
                    DateTime when = DateTimeOffset.FromUnixTimeSeconds(data.Timestamp).LocalDateTime;
                    player.ChatMessage(Msg("FirstBlood.Status.Claimed", player.UserIDString, data.KillerName, when.ToString("yyyy-MM-dd HH:mm")));
                    break;

                case "reset":
                    if (!permission.UserHasPermission(player.UserIDString, PermReset))
                    {
                        player.ChatMessage(Msg("FirstBlood.NoPermission", player.UserIDString));
                        return;
                    }
                    data = new FirstBloodData();
                    Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);

                    config.Claimed = false;
                    config.KillerId = string.Empty;
                    config.KillerName = string.Empty;
                    config.Timestamp = 0;
                    SaveConfig();

                    player.ChatMessage(Msg("FirstBlood.Reset", player.UserIDString));
                    break;
            }
        }
    }
}

Hooks used: OnServerInitialized, OnNewSave, OnEntityDeath

Config file

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

FirstBlood.json
{
  "Announce First Blood": true,
  "Reward Enabled": true,
  "Reward Items": [
    {
      "Shortname": "scrap",
      "Amount": 250,
      "Skin": 0
    },
    {
      "Shortname": "supply.signal",
      "Amount": 1,
      "Skin": 0
    }
  ],
  "Claimed": false,
  "Killer Id": "",
  "Killer Name": "",
  "Timestamp": 0
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/firstblood resetClears the current first blood claim so the next kill counts again.firstblood.reset
/firstblood statusShows whether first blood has already been claimed and by whom.firstblood.admin
Permissions
PermissionGrants
firstblood.adminGrants access to /firstblood status.
firstblood.resetAllows clearing the first blood claim with /firstblood reset.

How the code works

  1. Detect player kills in OnEntityDeath

    The hook runs for every entity that dies, so filter early: cast entity as BasePlayer, reject nulls, NPCs (IsNpc), and anything not connected. Then read info.InitiatorPlayer and reject it when it is null, the same player as the victim, or an NPC. Checking info.Initiator instead would let turrets and traps award the bonus to whoever placed them, which is usually not what an admin wants. Filters sit at the top of the method so the rest of the logic runs only for clean PvP kills.

  2. Persist the claim across restarts

    A static bool would reset every time the plugin reloads, which lets players farm the reward by triggering reloads. Instead the claim lives in a small data class saved through Interface.Oxide.DataFileSystem.WriteObject("FirstBlood", data). It stores whether first blood was taken, the killer's UserIDString, the display name, and a unix timestamp from Facepunch.Math.Epoch.Current. On OnServerInitialized you read it back with ReadObject<FirstBloodData> and fall back to a fresh object when it returns null.

  3. Announce and reward the killer

    Once the claim is set and saved, build the broadcast from the language file: killer name, victim name, distance from info.ProjectileDistance rounded to a whole number, and weapon from info.WeaponPrefab when available. Server.Broadcast hits all connected players. The reward loop reads Reward Items from config, calls ItemManager.CreateByName(shortname, amount, skin) and checks for null before player.GiveItem(item), since an unknown shortname silently returns null and would otherwise throw.

  4. Configure items, broadcast and reward toggle

    The nested PluginConfig class exposes Announce First Blood, Reward Enabled, and a Reward Items list of RewardItem entries with Shortname, Amount and Skin. Defaults ship with scrap and a supply signal. Because Config.ReadObject runs before the file is written the first time, LoadConfig catches a null result and calls LoadDefaultConfig so the file is created with sane values instead of throwing on every reload.

  5. Reset on wipe with OnNewSave

    OnNewSave fires when the server generates a new save, which in practice means a wipe. The hook clears the in-memory data object and writes the empty version back so the next kill after the wipe is first blood again. This keeps the plugin correct without an admin touching the file. Do not call Puts or PrintWarning inside OnEntityDeath; the only startup logging happens in OnServerInitialized when the loaded state is reported once.

  6. Admin commands and permissions

    /firstblood status replies with the stored killer name and timestamp in local time, useful when players dispute a claim. /firstblood reset with the firstblood.reset permission clears the data and saves it. Both are [ChatCommand] methods that check permission.UserHasPermission(player.UserIDString, ...) before doing anything, and every reply comes from lang.GetMessage so admins can translate the plugin without a recompile.

Install it on your server

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

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

Variations

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

  • Reward with Economics money

    Pays a configurable currency amount instead of items using the Economics plugin reference.

    Generate this version
  • Team-aware first blood

    Only counts kills against players outside the killer's team.

    Generate this version
  • Daily first blood

    Resets the claim every 24 hours instead of on restart.

    Generate this version

FAQ

Does the plugin hand out two rewards if the server restarts?

No. The claim is written to oxide/data/FirstBlood.json the moment it happens, and the plugin reads that back on startup. Only OnNewSave (a wipe) or /firstblood reset clears it. Deleting the data file manually also works if you want to test the announcement.

Will a trap or turret kill count as first blood?

Only if the damage is attributed to a real player. The plugin reads info.InitiatorPlayer, which is populated when a player is the source of the damage. If you want strict gun and melee kills only, add a check on info.damageTypes.GetMajorityDamageType() for Bullet, Slash, Stab or Blunt before claiming.

Can I make first blood only count during the first hour of a wipe?

Yes. Add a config key for a window in seconds and compare Facepunch.Math.Epoch.Current against the wipe timestamp stored when the plugin saved its data. After the window expires, skip the announcement and mark the claim as expired so the reward cannot be farmed late in the day.

How do I install it on Oxide or Carbon?

Drop FirstBlood.cs into oxide/plugins, wait for the compile, then edit oxide/config/FirstBlood.json. Carbon loads the same file from carbon/plugins. Give yourself oxide.grant user <steamid> firstblood.reset to test the reset command.

Why does the broadcast include a distance that says 0m?

info.ProjectileDistance is the projectile's travel distance from the shooter to the victim; it is zero for melee, fall, and some explosion damage. The template formats it as an integer and it stays accurate enough to read. If it bothers you, hide the distance when info.ProjectileDistance is below 1.

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 FirstBlood Oxide plugin that announces the first player kill after a restart, grants configurable item rewards, persists the claim in oxide/data, and resets it on OnNewSave with admin reset and status chat commands.
Customize for my server

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

Last updated Oct 26, 2026.

Customize first blood

Customize