How to make a Rust headshot announcement plugin (bonus item on headshot kill)
Announces headshot kills in chat with killer, victim, weapon and distance, and grants a configurable bonus item when the shot traveled at least a minimum distance. Uses OnEntityDeath and reads HitInfo.isHeadshot.
Headshots are the most contested moment in Rust PvP, but the server gives no feedback beyond a kill line. Admins who want to reward precision have to watch the kill feed manually. This plugin hooks OnEntityDeath, checks that the dying entity is a player and that HitInfo.isHeadshot is true, then reads Weapon.GetItem().info.displayName.english and ProjectileDistance from the same HitInfo. If the distance meets the configured threshold it broadcasts a formatted line and gives the killer a stack of the configured bonus item. Short-point-blank headshots still announce if you leave the threshold at zero, and each part of the message is a language key so you can translate or restyle it. Everything is gated behind a permission and a toggle in the config, so you can hand the announcement to everyone and restrict the reward separately.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Headshot Rewards", "RustPluginGenerator", "1.0.0")]
[Description("Announces headshot kills in chat and grants a bonus item past a minimum distance")]
public class HeadshotRewards : RustPlugin
{
private const string PermAnnounce = "headshotrewards.announce";
private const string PermReward = "headshotrewards.reward";
private const string PermAdmin = "headshotrewards.admin";
private PluginConfig config;
private class PluginConfig
{
[JsonProperty("Announce headshots")]
public bool Announce { get; set; } = true;
[JsonProperty("Give bonus item")]
public bool GiveReward { get; set; } = true;
[JsonProperty("Minimum distance")]
public float MinimumDistance { get; set; } = 25f;
[JsonProperty("Skip team kills")]
public bool SkipTeamKills { get; set; } = true;
[JsonProperty("Bonus shortname")]
public string BonusShortname { get; set; } = "scrap";
[JsonProperty("Bonus amount")]
public int BonusAmount { get; set; } = 50;
[JsonProperty("Bonus skin")]
public ulong BonusSkin { get; set; } = 0UL;
}
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>
{
["Announce"] = "<color=#ff6b6b>HEADSHOT</color> {0} eliminated {1} with {2} from {3}m",
["Unknown"] = "unknown weapon",
["Reload"] = "Headshot Rewards reloaded. Minimum distance: {0}m, bonus: {1} x {2}"
}, 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(PermAnnounce, this);
permission.RegisterPermission(PermReward, this);
permission.RegisterPermission(PermAdmin, this);
}
private void OnServerInitialized()
{
if (!config.GiveReward) return;
if (ItemManager.FindItemDefinition(config.BonusShortname) == null)
PrintWarning($"Bonus shortname '{config.BonusShortname}' does not exist; no item will be given");
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (config == null || info == null) return;
var victim = entity as BasePlayer;
if (victim == null) return;
var killer = info.InitiatorPlayer;
if (killer == null || killer == victim) return;
if (!info.isHeadshot) return;
if (info.ProjectileDistance < config.MinimumDistance) return;
bool canAnnounce = config.Announce && permission.UserHasPermission(killer.UserIDString, PermAnnounce);
bool canReward = config.GiveReward && permission.UserHasPermission(killer.UserIDString, PermReward);
if (!canAnnounce && !canReward) return;
if (config.SkipTeamKills && killer.currentTeam != 0 && killer.currentTeam == victim.currentTeam) return;
string weapon = "weapon";
var item = info.Weapon != null ? info.Weapon.GetItem() : null;
if (item != null && item.info != null)
weapon = item.info.displayName.english;
else
weapon = Msg("Unknown");
if (canAnnounce)
{
Server.Broadcast(Msg("Announce", null,
killer.displayName,
victim.displayName,
weapon,
Math.Round(info.ProjectileDistance, 1)));
}
if (canReward)
GiveBonus(killer);
}
private void GiveBonus(BasePlayer player)
{
var item = ItemManager.CreateByName(config.BonusShortname, Math.Max(1, config.BonusAmount), config.BonusSkin);
if (item == null) return;
player.GiveItem(item);
}
[ChatCommand("hsreload")]
private void CmdReload(BasePlayer player, string command, string[] args)
{
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, "You do not have permission to use this command");
return;
}
LoadConfig();
SendReply(player, Msg("Reload", player?.UserIDString,
config.MinimumDistance, config.BonusAmount, config.BonusShortname));
}
}
}Hooks used: OnServerInitialized, OnEntityDeath
Config file
Created automatically on first load at oxide/config/HeadshotRewards.json.
{
"Announce headshots": true,
"Give bonus item": true,
"Minimum distance": 25,
"Skip team kills": true,
"Bonus shortname": "scrap",
"Bonus amount": 50,
"Bonus skin": 0
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/hsreload | Reloads the plugin config from disk and prints the current minimum distance and bonus item. | headshotrewards.admin |
| Permission | Grants |
|---|---|
headshotrewards.announce | Kills made by this player are announced in chat. |
headshotrewards.reward | Kills made by this player grant the configured bonus item. |
headshotrewards.admin | Allows the /hsreload chat command. |
How the code works
Filter for player victims
OnEntityDeath(BaseCombatEntity entity, HitInfo info)fires for every entity that dies, including animals, Bradley and building parts. The first checks reject anything that is not aBasePlayer, has noinfo, or has no attacker.info.InitiatorPlayeris only set when the attacker was a player, so it also filters out NPC and turret kills. Returning early on these cases keeps the hook cheap, which matters because it runs on every death on the server.Confirm the headshot and read the distance
info.isHeadshotis the same flag the game uses for the headshot sound and the damage multiplier.info.ProjectileDistanceholds how far the projectile traveled before impact, in metres; for melee and non-projectile damage it is zero, so the minimum distance threshold naturally filters those out. Weapon name comes frominfo.Weapon.GetItem(), which is null for some damage sources, so fall back to a language key such asUnknown.Broadcast through the language file
The announce line uses
Server.BroadcastwithMsg("Announce", null, killer, victim, weapon, distance). Passingnullas the user id makeslang.GetMessagereturn the default language entry instead of a per-player one, which is what you want for a server-wide message. Placeholders are positional:{0}killer,{1}victim,{2}weapon,{3}distance rounded to one decimal.Grant the bonus item safely
The reward uses
ItemManager.CreateByName(config.BonusShortname, config.BonusAmount, config.BonusSkin)and thenplayer.GiveItem(item), which drops the item at the player's feet if the inventory is full.CreateByNamereturns null for an unknown shortname, so the config is validated inOnServerInitializedand a warning is printed once at startup instead of on every kill. Only the reward permission holder receives the item.Config keys that control the behaviour
Minimum distancegates both announcement and reward; set it to0to announce every headshot.Announce headshotsandGive bonus itemare independent switches.Bonus shortname,Bonus amountandBonus skindefine the item, andSkip team killsprevents an announcement when killer and victim share a non-zerocurrentTeam, which stops squad members from farming each other.Permissions and the reload command
Three permissions are registered in
Init:headshotrewards.announce,headshotrewards.rewardandheadshotrewards.admin. The hook checks them withpermission.UserHasPermission(player.UserIDString, ...)./hsreloadis a[ChatCommand]that callsLoadConfig()again and replies with the active settings, so you can tune the distance without restarting the server.
Install it on your server
Save HeadshotRewards.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/HeadshotRewards.json(Carbon:carbon/configs/HeadshotRewards.json). After editing, reload it:oxide.reload HeadshotRewards c.reload HeadshotRewards # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default headshotrewards.announce c.grant group default headshotrewards.announce # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Scrap payout via Economics
Replace the item reward with a configurable currency deposit through the Economics plugin.
Generate this versionDistance tiers
Announce with a different message and reward per distance bracket.
Generate this versionNo-scope and weapon filter
Only announce headshots made with specific weapons or without a scope.
Generate this version
FAQ
Why does the announcement never fire?
Most likely info.isHeadshot is false or info.InitiatorPlayer is null. Turret, trap and NPC kills have no player initiator. Also check that the killer holds headshotrewards.announce and that Minimum distance is not higher than the shots you are taking.
Does the minimum distance apply to the reward as well?
Yes. The check runs once, before both the broadcast and the item grant, so a close-range headshot below the threshold is silent and unrewarded. Set Minimum distance to 0 if you want every headshot to count and rely on the permission instead.
What happens if the bonus shortname is wrong?
ItemManager.CreateByName returns null and no item is given. The plugin validates the shortname in OnServerInitialized and prints a one-time warning to the server console listing the invalid value, so the message does not spam the log on each kill.
Can teammates farm each other for the bonus?
Not with Skip team kills enabled. When both players have a currentTeam other than 0 and it matches, the hook returns early. Note that this only covers the in-game team system, not clan plugins; if you run one, add its API to the same check.
How do I install it on Oxide or Carbon?
Drop HeadshotRewards.cs into oxide/plugins, wait for compilation, then grant permissions with oxide.grant user <steamid> headshotrewards.announce and the same for headshotrewards.reward. Edit oxide/config/HeadshotRewards.json and run /hsreload. Carbon loads the same file from its plugins folder.
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 pvp plugins
- Combat blockTracks combat with the OnEntityTakeDamage hook and blocks a configurable list of chat commands while a player is in combat, using OnPlayerCommand toโฆOnEntityTakeDamageOnPlayerCommand
- DuelsAdds a player-vs-player duel system where /duel <player> sends a challenge, /duel accept starts it, and damage outside the duel pair is blocked.OnEntityTakeDamageOnEntityDeathOnPlayerDisconnected
- Friendly fire protectionBlocks damage between members of the same Rust team using OnEntityTakeDamage, with a permission whitelist and a /ff toggle for team leaders thatโฆOnEntityTakeDamageOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize headshot announcements
Customize