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
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.
{
"Alert Throttle Seconds": 60,
"Alert On Decay Damage": false,
"Prune Interval Seconds": 300
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/raidalerts | Toggle your own raid alerts on or off for the current session. | Everyone |
| Permission | Grants |
|---|---|
raidalerts.use | Allows toggling raid alerts with /raidalerts. |
How the code works
Filter to explosive building damage
OnEntityTakeDamagefires for every hit in the game, so returnnullquickly unlesshitInfois non-null andhitInfo.damageTypes.GetMajorityDamageType()isRust.DamageType.Explosion. Then castentitytoBuildingBlock; anything else (players, animals, deployables) is ignored. Readblock.OwnerIDandblock.buildingIDfrom the affected block and bail if both are zero, which covers unowned and decayed structures.Resolve the building privilege and attackers
BuildingBlock.GetBuildingPrivilege()returns theBuildingPrivlidgefor 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 fromhitInfo.InitiatorPlayerfirst; if the hit came from a thrown explosive or rocket that has no direct player initiator, fall back tohitInfo.Initiatorand itsOwnerID. Skip the alert entirely when no attacker resolves, since a nameless warning is not useful.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 withBasePlayer.FindByID. Drop the attacker and any teammate who sharescurrentTeamwith the attacker, and honour the session-based opt-out set. Each recipient getsMsg("RaidAlert", id, ...)so the text stays in the language file.Throttle per building
A rocket volley triggers dozens of damage events on different blocks of the same structure. Keep a
Dictionary<ulong, float>frombuildingIDto the last alert time usingUnityEngine.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 atimer.Everyyou keep a reference to and destroy inUnload.Format the message with grid and attacker
The map grid comes from
hitInfo.HitPositionWorldthroughPositionToGrid, 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.Config, permissions and toggle command
PluginConfigexposesAlert Throttle Seconds,Alert On Decay Damage(default false) andPrune Interval Secondsas[JsonProperty]fields, withLoadDefaultConfigseeding them andLoadConfigfalling back to defaults on parse errors.Initregistersraidalerts.use;/raidalertstoggles 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
Save RaidAlerts.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/RaidAlerts.json(Carbon:carbon/configs/RaidAlerts.json). After editing, reload it:oxide.reload RaidAlerts c.reload RaidAlerts # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionOnly warn when a cupboard has TC auth
Skip alerts for structures with no tool cupboard so decayed or abandoned builds stay quiet.
Generate this versionTeam-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More raiding plugins
- Explosive damage multiplierScales explosion damage against building blocks, deployables and other structures using per-weapon multipliers keyed by the attacker's weapon prefabโฆOnEntityTakeDamage
- New player protectionBlocks damage to buildings owned by players under a configurable playtime threshold, tracks playtime per player in oxide/data, and removesโฆOnServerInitializedOnNewSaveOnEntityTakeDamage
- Offline raid protectionScales damage to building blocks and deployables when every player authorized on the covering tool cupboard has been offline for a configurable delay.OnServerInitializedOnCupboardAuthorizeOnCupboardDeauthorize
Last updated Oct 26, 2026.
Customize raid alerts
Customize