Skip to content
RustPluginGenerator

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

BossKillAnnouncer.cs
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.

BossKillAnnouncer.json
{
  "Announcements Enabled": true,
  "Prefix": "[Boss]",
  "Show Bradley": true,
  "Show Helicopter": true,
  "Grid In Messages": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/bosskillsShows whether announcements are enabled and where the last announcement went.bosskillannouncer.admin
Permissions
PermissionGrants
bosskillannouncer.adminGrants access to the /bosskills status command.

How the code works

  1. Filtering the target entities

    Both hooks start with the same type check: entity as BradleyAPC for the APC and entity as PatrolHelicopter for the helicopter. Only those two types continue, so every other damage event returns null immediately and default damage behavior is untouched. Patrol helicopters are actually BaseHelicopter subclasses, so the cast to PatrolHelicopter is what separates the event heli from minicopters. If you want to add more bosses later, extend the same cast check.

  2. Recording damage per player

    In OnEntityTakeDamage the plugin reads hitInfo.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 by entity.net.ID.Value, with a nested dictionary of player.userID to total damage. Damage is recorded as-is from hitInfo.damageTypes.Total() before any scaling, which is fine for ranking purposes. All calls happen on the main thread so no locking is needed.

  3. Resolving the winner on death

    OnEntityDeath looks up the stored tally for that entity ID and keeps the player with the largest total. That player is looked up with BasePlayer.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.

  4. Computing the map grid

    The grid is derived from entity.transform.position using 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.worldsize gives the current map size, so custom-size maps still resolve correctly. The result is a string like H14 and is only included when Grid In Messages is enabled in the config.

  5. Formatting the chat line

    The message template lives in LoadDefaultMessages under the key Kill Message and contains {0} for the killer, {1} for the boss name and {2} for the grid. Sending goes through Server.Broadcast, which reaches every connected player including the killer. The Prefix config 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.

  6. Configuration and the admin command

    LoadDefaultConfig seeds Announcements Enabled, Prefix, Show Bradley, Show Helicopter and Grid 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 /bosskills chat command requires bosskillannouncer.admin and replies with the current enabled state and the value of Prefix, which is useful when someone reports that announcements stopped.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Team-wide credit

    Credit the whole team of the top damage dealer in the announcement.

    Generate this version
  • Discord 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.

Prompt: Create a Rust Oxide plugin that tracks player damage on Bradley APC and patrol helicopter, then broadcasts a chat message naming the top damage dealer and the death grid, with configurable prefix and per-boss toggles.
Customize for my server

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

Last updated Oct 26, 2026.

Customize bradley and heli kill announcer

Customize