Skip to content
RustPluginGenerator

How to make a Rust raid alert plugin (chat warnings for base owners)

Chat-alerts online owners and tool cupboard authed players when explosives damage their building, with the attacker name and map grid, throttled to one alert per building per minute. It uses OnEntityTakeDamage and reads the building's BuildingPrivlidge list.

Raid alerts are the difference between a base that gets quietly blown into and one the owners can defend. The straightforward approach is to listen on OnEntityTakeDamage, filter for explosion damage on building blocks, and resolve who owns the structure through its building privilege. The tricky parts are throttle and audience: a single rocket produces many damage events across several blocks, so you keep a last-alert timestamp keyed by building ID and only message the online owner plus anyone authed on the cupboard, once per minute.

The plugin below does that. It checks the hit is explosive, skips decay and non-building entities, resolves the building's BuildingPrivlidge with GetBuildingPrivilege(), checks the attacker is not the owner or an authed player, then sends a lang-based chat line with the attacker name and the map grid of the impact. Alerts are throttled per building ID and pruned to stay small. A /raidalerts toggle lets admins and players with permission turn their own alerts off.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Raid Alerts", "RustPluginGenerator", "1.0.0")]
    [Description("Warns online owners and tool cupboard authed players in chat when explosives damage their building.")]
    public class RaidAlerts : RustPlugin
    {
        private const string PermUse = "raidalerts.use";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> lastAlert = new Dictionary<ulong, float>();
        private readonly HashSet<ulong> optedOut = new HashSet<ulong>();
        private Timer pruneTimer;

        private class PluginConfig
        {
            [JsonProperty("Alert Throttle Seconds")]
            public float AlertThrottleSeconds { get; set; } = 60f;

            [JsonProperty("Alert On Decay Damage")]
            public bool AlertOnDecayDamage { get; set; } = false;

            [JsonProperty("Prune Interval Seconds")]
            public float PruneIntervalSeconds { get; set; } = 300f;
        }

        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>
            {
                ["RaidAlert"] = "<color=#ff6b6b>[Raid]</color> {0} is damaging your base at grid {1}",
                ["RaidAlertUnknown"] = "<color=#ff6b6b>[Raid]</color> Your base is under attack at grid {1}",
                ["ToggleOn"] = "Raid alerts are now <color=#7ee787>enabled</color>.",
                ["ToggleOff"] = "Raid alerts are now <color=#ffa657>disabled</color>.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

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

        private void OnServerInitialized()
        {
            pruneTimer = timer.Every(Mathf.Max(30f, config.PruneIntervalSeconds), PruneThrottle);
        }

        private void Unload()
        {
            pruneTimer?.Destroy();
        }

        private void PruneThrottle()
        {
            if (lastAlert.Count == 0) return;
            float now = UnityEngine.Time.realtimeSinceStartup;
            float cutoff = Mathf.Max(config.AlertThrottleSeconds * 5f, 600f);
            var stale = new List<ulong>();
            foreach (var entry in lastAlert)
            {
                if (now - entry.Value > cutoff) stale.Add(entry.Key);
            }
            foreach (var id in stale) lastAlert.Remove(id);
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (hitInfo == null || entity == null) return null;
            var block = entity as BuildingBlock;
            if (block == null) return null;
            if (hitInfo.damageTypes == null) return null;

            var major = hitInfo.damageTypes.GetMajorityDamageType();
            if (major != Rust.DamageType.Explosion)
            {
                if (!config.AlertOnDecayDamage || major != Rust.DamageType.Decay) return null;
            }

            ulong buildingId = block.buildingID;
            if (buildingId == 0 && block.OwnerID != 0) buildingId = block.OwnerID;
            if (buildingId == 0) return null;

            float now = UnityEngine.Time.realtimeSinceStartup;
            float last;
            if (lastAlert.TryGetValue(buildingId, out last) && now - last < config.AlertThrottleSeconds) return null;

            BasePlayer attacker = hitInfo.InitiatorPlayer;
            ulong attackerId = attacker != null ? attacker.userID.Get() : (hitInfo.Initiator != null ? hitInfo.Initiator.OwnerID : 0);
            string attackerName = attacker != null ? attacker.displayName : null;
            if (attackerId == 0) return null;

            var privilege = block.GetBuildingPrivilege();
            var audience = new HashSet<ulong>();
            ulong ownerId = block.OwnerID;
            if (privilege != null)
            {
                if (privilege.OwnerID != 0) ownerId = privilege.OwnerID;
                foreach (var authorized in privilege.authorizedPlayers)
                {
                    ulong authorizedId = authorized;
                    if (authorizedId != 0) audience.Add(authorizedId);
                }
            }
            if (ownerId != 0) audience.Add(ownerId);
            if (audience.Count == 0) return null;

            string grid = PositionToGrid(hitInfo.HitPositionWorld);
            ulong attackerTeam = attacker != null ? attacker.currentTeam : 0;
            bool sent = false;

            foreach (var userId in audience)
            {
                if (userId == attackerId) continue;
                if (optedOut.Contains(userId)) continue;
                var target = BasePlayer.FindByID(userId);
                if (target == null || !target.IsConnected) continue;
                if (attackerTeam != 0 && target.currentTeam == attackerTeam) continue;

                if (attackerName != null)
                    target.ChatMessage(Msg("RaidAlert", target.UserIDString, attackerName, grid));
                else
                    target.ChatMessage(Msg("RaidAlertUnknown", target.UserIDString, grid));
                sent = true;
            }

            if (sent) lastAlert[buildingId] = now;
            return null;
        }

        private static string PositionToGrid(Vector3 position)
        {
            if (World.Size <= 0f) return "?";
            float cellSize = World.Size / 146.3f;
            int x = Mathf.FloorToInt((position.x + World.Size * 0.5f) / cellSize);
            int z = Mathf.FloorToInt((position.z + World.Size * 0.5f) / cellSize);
            x = Mathf.Clamp(x, 0, 145);
            z = Mathf.Clamp(z, 0, 145);
            return ((char)('A' + x)).ToString() + (146 - z).ToString();
        }

        [ChatCommand("raidalerts")]
        private void CmdRaidAlerts(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            ulong id = player.userID.Get();
            if (!optedOut.Remove(id))
            {
                optedOut.Add(id);
                player.ChatMessage(Msg("ToggleOff", player.UserIDString));
            }
            else
            {
                player.ChatMessage(Msg("ToggleOn", player.UserIDString));
            }
        }
    }
}

Hooks used: OnServerInitialized, OnEntityTakeDamage

Config file

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

RaidAlerts.json
{
  "Alert Throttle Seconds": 60,
  "Alert On Decay Damage": false,
  "Prune Interval Seconds": 300
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/raidalertsToggle your own raid alerts on or off for the current session.Everyone
Permissions
PermissionGrants
raidalerts.useAllows toggling raid alerts with /raidalerts.

How the code works

  1. Filter to explosive building damage

    OnEntityTakeDamage fires for every hit in the game, so return null quickly unless hitInfo is non-null and hitInfo.damageTypes.GetMajorityDamageType() is Rust.DamageType.Explosion. Then cast entity to BuildingBlock; anything else (players, animals, deployables) is ignored. Read block.OwnerID and block.buildingID from the affected block and bail if both are zero, which covers unowned and decayed structures.

  2. Resolve the building privilege and attackers

    BuildingBlock.GetBuildingPrivilege() returns the BuildingPrivlidge for the structure, or null when there is no cupboard. Use it to get the authed player list and the cupboard's owner. Pull the attacker from hitInfo.InitiatorPlayer first; if the hit came from a thrown explosive or rocket that has no direct player initiator, fall back to hitInfo.Initiator and its OwnerID. Skip the alert entirely when no attacker resolves, since a nameless warning is not useful.

  3. Build the audience

    Collect the owner (from the block or cupboard) and every ID in privilege.authorizedPlayers, then message the ones who are currently connected with BasePlayer.FindByID. Drop the attacker and any teammate who shares currentTeam with the attacker, and honour the session-based opt-out set. Each recipient gets Msg("RaidAlert", id, ...) so the text stays in the language file.

  4. Throttle per building

    A rocket volley triggers dozens of damage events on different blocks of the same structure. Keep a Dictionary<ulong, float> from buildingID to the last alert time using UnityEngine.Time.realtimeSinceStartup. If the last alert was under 60 seconds ago, return without messaging. To avoid unbounded growth, prune entries older than a few minutes inside the same hook, or on a timer.Every you keep a reference to and destroy in Unload.

  5. Format the message with grid and attacker

    The map grid comes from hitInfo.HitPositionWorld through PositionToGrid, which converts world position to the familiar letter/number format using the world size divided into 146.3m squares. The message template in the lang file takes the attacker name and the grid, for example {0} is damaging your base at grid {1}. Keep a distinct line for when the attacker cannot be named, but with the fallback above that branch is rare.

  6. Config, permissions and toggle command

    PluginConfig exposes Alert Throttle Seconds, Alert On Decay Damage (default false) and Prune Interval Seconds as [JsonProperty] fields, with LoadDefaultConfig seeding them and LoadConfig falling back to defaults on parse errors. Init registers raidalerts.use; /raidalerts toggles the calling player's entry in the opt-out set. There is no persistence: opt-outs reset when the plugin reloads, which keeps the file small and avoids stale preferences.

Install it on your server

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

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

Variations

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

  • Discord webhook relay

    Forward the same alert to a Discord channel through a configurable webhook URL.

    Generate this version
  • Only warn when a cupboard has TC auth

    Skip alerts for structures with no tool cupboard so decayed or abandoned builds stay quiet.

    Generate this version
  • Team-wide offline queue

    Store missed alerts and deliver them when the owner next connects.

    Generate this version

FAQ

Why does the alert only fire once per minute?

Because a single rocket salvo touches many blocks and would otherwise spam chat dozens of times. The throttle key is the buildingID from the damaged BuildingBlock, so separate structures on the same base still get their own alert after the minute is up. Adjust the Alert Throttle Seconds config value if you want a different window.

How do I install it on Oxide or Carbon?

Drop the compiled .cs file into oxide/plugins, or carbon/plugins on Carbon, and let the server load it. The config file is generated at oxide/config/RaidAlerts.json on first load. Edit it and run oxide.reload RaidAlerts to apply changes. No other plugins are required.

Does the plugin tell me who the attacker is if they use a rocket launcher from far away?

It reads hitInfo.InitiatorPlayer first, which is set for player-fired rockets and thrown explosives. If that is null it falls back to the entity's OwnerID. Anonymous environmental explosions (for example a Bradley shell) will not resolve to a player and are skipped instead of sending an empty alert.

Will my own team's explosions trigger alerts?

Not to you. The code compares the attacker's userID against the owner and each authed player, and also against their currentTeam, so teammates who are raiding together do not get warned about each other. Raiders get no message; only defenders who are not the attacker do.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create an Oxide Rust plugin called RaidAlerts that messages online owners and tool cupboard authed players in chat when explosive damage hits their building, showing attacker name and map grid, throttled to one alert per building per 60 seconds, with /raidalerts toggle.
Customize for my server

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

Last updated Oct 26, 2026.

Customize raid alerts

Customize