How to make a Rust Bradley APC and patrol helicopter kill announcer plugin
Tracks 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 boss died. Uses OnEntityTakeDamage and OnEntityDeath.
Bradley APC and patrol helicopter kills are the big PvE moments on most servers, but the default chat gives no credit to whoever actually did the work. Several players can be shooting at the same target, and the loot goes to the last hit or the person who looted first. This plugin keeps a per-target damage tally and, when the entity dies, picks the player with the highest accumulated damage as the killer. It prints one chat line with the killer's name, the entity type and the map grid computed from the death position. Damage from NPCs, decay or turrets is ignored because only player damage counts. Tally data is kept in memory keyed by the entity's network ID and removed on death so nothing leaks between events. Two short settings control the message prefix and whether the grid is shown.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Boss Kill Announcer", "RustPluginGenerator", "1.0.0")]
[Description("Announces who dealt the most damage to a Bradley APC or patrol helicopter and where it died.")]
public class BossKillAnnouncer : RustPlugin
{
private const string PermAdmin = "bosskillannouncer.admin";
private PluginConfig config;
// entity net id -> (player id -> total damage)
private readonly Dictionary<ulong, Dictionary<ulong, float>> damage = new Dictionary<ulong, Dictionary<ulong, float>>();
private class PluginConfig
{
[JsonProperty("Announcements Enabled")]
public bool AnnouncementsEnabled { get; set; } = true;
[JsonProperty("Prefix")]
public string Prefix { get; set; } = "[Boss]";
[JsonProperty("Show Bradley")]
public bool ShowBradley { get; set; } = true;
[JsonProperty("Show Helicopter")]
public bool ShowHelicopter { get; set; } = true;
[JsonProperty("Grid In Messages")]
public bool GridInMessages { get; set; } = true;
}
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>
{
["Kill Message"] = "{0} {1} was destroyed by {2} (grid {3})",
["Status"] = "{0} announcements are {1}.",
["On"] = "enabled",
["Off"] = "disabled"
}, 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(PermAdmin, this);
}
private void Unload()
{
damage.Clear();
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null || hitInfo == null || hitInfo.InitiatorPlayer == null) return null;
if (!(entity is BradleyAPC) && !(entity is PatrolHelicopter)) return null;
var attacker = hitInfo.InitiatorPlayer;
if (attacker.IsNpc) return null;
ulong entityId = entity.net.ID.Value;
ulong playerId = attacker.userID;
Dictionary<ulong, float> tally;
if (!damage.TryGetValue(entityId, out tally))
{
tally = new Dictionary<ulong, float>();
damage[entityId] = tally;
}
float amount;
tally.TryGetValue(playerId, out amount);
tally[playerId] = amount + hitInfo.damageTypes.Total();
return null;
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (entity == null) return;
if (!(entity is BradleyAPC) && !(entity is PatrolHelicopter)) return;
ulong entityId = entity.net.ID.Value;
Dictionary<ulong, float> tally;
if (!damage.TryGetValue(entityId, out tally))
{
return;
}
damage.Remove(entityId);
ulong bestPlayer = 0;
float bestDamage = 0f;
foreach (var entry in tally)
{
if (entry.Value > bestDamage)
{
bestDamage = entry.Value;
bestPlayer = entry.Key;
}
}
if (bestPlayer == 0 || !config.AnnouncementsEnabled) return;
bool isBradley = entity is BradleyAPC;
if (isBradley && !config.ShowBradley) return;
if (!isBradley && !config.ShowHelicopter) return;
var killer = BasePlayer.FindAwakeOrSleeping(bestPlayer.ToString());
string killerName = killer != null ? killer.displayName : bestPlayer.ToString();
string bossName = isBradley ? "Bradley APC" : "Patrol Helicopter";
string grid = config.GridInMessages ? GetGrid(entity.transform.position) : "?";
Server.Broadcast(Msg("Kill Message", null, config.Prefix, bossName, killerName, grid));
}
[ChatCommand("bosskills")]
private void CmdBossKills(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermAdmin)) return;
string state = config.AnnouncementsEnabled ? Msg("On", player.UserIDString) : Msg("Off", player.UserIDString);
SendReply(player, Msg("Status", player.UserIDString, config.Prefix, state));
}
private static string GetGrid(Vector3 position)
{
float worldSize = ConVar.Server.worldsize;
if (worldSize <= 0f) worldSize = 4000f;
float half = worldSize * 0.5f;
float cellSize = worldSize / 26.6f;
int x = Mathf.FloorToInt((position.x + half) / cellSize);
int z = Mathf.FloorToInt((position.z + half) / cellSize);
char letter = (char)('A' + Mathf.Clamp(x, 0, 25));
int number = Mathf.Clamp(z + 1, 1, 26);
return letter.ToString() + number;
}
}
}Hooks used: OnEntityTakeDamage, OnEntityDeath
Config file
Created automatically on first load at oxide/config/BossKillAnnouncer.json.
{
"Announcements Enabled": true,
"Prefix": "[Boss]",
"Show Bradley": true,
"Show Helicopter": true,
"Grid In Messages": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/bosskills | Shows whether announcements are enabled and where the last announcement went. | bosskillannouncer.admin |
| Permission | Grants |
|---|---|
bosskillannouncer.admin | Grants access to the /bosskills status command. |
How the code works
Filtering the target entities
Both hooks start with the same type check:
entity as BradleyAPCfor the APC andentity as PatrolHelicopterfor the helicopter. Only those two types continue, so every other damage event returnsnullimmediately and default damage behavior is untouched. Patrol helicopters are actuallyBaseHelicoptersubclasses, so the cast toPatrolHelicopteris what separates the event heli from minicopters. If you want to add more bosses later, extend the same cast check.Recording damage per player
In
OnEntityTakeDamagethe plugin readshitInfo.InitiatorPlayer. When that is null (turrets, NPCs, decay, explosions with no owner) nothing is recorded. When it is set, the attacker's damage is added to a dictionary entry keyed byentity.net.ID.Value, with a nested dictionary ofplayer.userIDto total damage. Damage is recorded as-is fromhitInfo.damageTypes.Total()before any scaling, which is fine for ranking purposes. All calls happen on the main thread so no locking is needed.Resolving the winner on death
OnEntityDeathlooks up the stored tally for that entity ID and keeps the player with the largest total. That player is looked up withBasePlayer.FindAwakeOrSleeping(id.ToString())so the message still works if they disconnected mid-fight. If no player damage was recorded, the plugin stays silent rather than announcing an empty kill. The tally entry is removed from the dictionary in every case so the data does not grow over a wipe.Computing the map grid
The grid is derived from
entity.transform.positionusing the standard Rust grid math: add half the world size, divide by the grid cell size, add 1 and convert to a letter for the X axis and a number for the Z axis.ConVar.Server.worldsizegives the current map size, so custom-size maps still resolve correctly. The result is a string likeH14and is only included whenGrid In Messagesis enabled in the config.Formatting the chat line
The message template lives in
LoadDefaultMessagesunder the keyKill Messageand contains{0}for the killer,{1}for the boss name and{2}for the grid. Sending goes throughServer.Broadcast, which reaches every connected player including the killer. ThePrefixconfig value is prepended inside the same template so admins can change the tag without editing code. Colors are not embedded because the default chat handler strips them for some clients.Configuration and the admin command
LoadDefaultConfigseedsAnnouncements Enabled,Prefix,Show Bradley,Show HelicopterandGrid In Messages. Each flag is checked before a message is broadcast, so an admin can turn off APC announcements without removing the code path. The/bosskillschat command requiresbosskillannouncer.adminand replies with the current enabled state and the value ofPrefix, which is useful when someone reports that announcements stopped.
Install it on your server
Save BossKillAnnouncer.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/BossKillAnnouncer.json(Carbon:carbon/configs/BossKillAnnouncer.json). After editing, reload it:oxide.reload BossKillAnnouncer c.reload BossKillAnnouncer # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default bosskillannouncer.admin c.grant group default bosskillannouncer.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Reward the top damage dealer
Give scrap or an item to the player with the most damage once the boss dies.
Generate this versionTeam-wide credit
Credit the whole team of the top damage dealer in the announcement.
Generate this versionDiscord webhook relay
Forward announcements to a Discord channel instead of only local chat.
Generate this version
FAQ
How do I install it on Oxide or Carbon?
Drop the compiled BossKillAnnouncer.cs into oxide/plugins, then run oxide.reload BossKillAnnouncer from the server console. Carbon loads the same file from its plugins folder. The default config is written to oxide/config/BossKillAnnouncer.json on first load and can be edited afterwards.
Why did no message appear after a Bradley kill?
Most likely no player damage was recorded, for example if the APC was destroyed by decay, an NPC or the environment. The plugin returns early when no player is credited. It also returns early if Announcements Enabled or Show Bradley is false in the config.
Does damage from turrets count for their owner?
No. The plugin only credits the damage when hitInfo.InitiatorPlayer is set, which requires the damage to be attributed to a connected player entity. Turrets and NPCs usually set this to null or to the turret entity, so their damage is skipped.
Will the tally survive a plugin reload?
No. Damage totals are kept only in memory and are cleared when the plugin unloads. That is intentional: a reload usually happens after a wipe or an update, and stale totals would credit the wrong player.
Does it work on custom map sizes?
Yes. Grid letters and numbers are computed from ConVar.Server.worldsize, so maps at 3000, 4500 or any other size resolve to the same grid label the game shows in the map UI.
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
- 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
- First bloodAnnounces the first player kill after a restart as First Blood and pays the killer a configured reward.OnServerInitializedOnNewSaveOnEntityDeath
- 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 bradley and heli kill announcer
Customize