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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/firstblood reset | Clears the current first blood claim so the next kill counts again. | firstblood.reset |
/firstblood status | Shows whether first blood has already been claimed and by whom. | firstblood.admin |
| Permission | Grants |
|---|---|
firstblood.admin | Grants access to /firstblood status. |
firstblood.reset | Allows clearing the first blood claim with /firstblood reset. |
How the code works
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 readinfo.InitiatorPlayerand reject it when it is null, the same player as the victim, or an NPC. Checkinginfo.Initiatorinstead 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.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'sUserIDString, the display name, and a unix timestamp fromFacepunch.Math.Epoch.Current. OnOnServerInitializedyou read it back withReadObject<FirstBloodData>and fall back to a fresh object when it returns null.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.ProjectileDistancerounded to a whole number, and weapon frominfo.WeaponPrefabwhen available.Server.Broadcasthits all connected players. The reward loop readsReward Itemsfrom config, callsItemManager.CreateByName(shortname, amount, skin)and checks for null beforeplayer.GiveItem(item), since an unknown shortname silently returns null and would otherwise throw.Configure items, broadcast and reward toggle
The nested
PluginConfigclass exposesAnnounce First Blood,Reward Enabled, and aReward Itemslist ofRewardItementries withShortname,AmountandSkin. Defaults ship with scrap and a supply signal. BecauseConfig.ReadObjectruns before the file is written the first time,LoadConfigcatches a null result and callsLoadDefaultConfigso the file is created with sane values instead of throwing on every reload.Reset on wipe with OnNewSave
OnNewSavefires when the server generates a new save, which in practice means a wipe. The hook clears the in-memorydataobject 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 callPutsorPrintWarninginsideOnEntityDeath; the only startup logging happens inOnServerInitializedwhen the loaded state is reported once.Admin commands and permissions
/firstblood statusreplies with the stored killer name and timestamp in local time, useful when players dispute a claim./firstblood resetwith thefirstblood.resetpermission clears the data and saves it. Both are[ChatCommand]methods that checkpermission.UserHasPermission(player.UserIDString, ...)before doing anything, and every reply comes fromlang.GetMessageso admins can translate the plugin without a recompile.
Install it on your server
Save FirstBlood.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/FirstBlood.json(Carbon:carbon/configs/FirstBlood.json). After editing, reload it:oxide.reload FirstBlood c.reload FirstBlood # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionTeam-aware first blood
Only counts kills against players outside the killer's team.
Generate this versionDaily 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More events plugins
- Bradley and heli kill announcerTracks damage dealt to the Bradley APC and patrol helicopter, then announces in chat which player did the most damage and the map grid where theโฆOnEntityTakeDamageOnEntityDeath
- Double gather hourRuns scheduled or admin-triggered double gather windows that multiply dispenser and collectible yields for a fixed duration, with start and endโฆOnServerInitializedOnDispenserGatherOnCollectiblePickup
- Locked crate drop eventSpawns a hackable locked crate at a random configured monument on a repeating timer, announces the monument in chat, and removes the previous eventโฆOnServerInitializedOnCrateHackOnEntityKill
Last updated Oct 26, 2026.
Customize first blood
Customize