How to make a Rust friendly fire plugin (team damage blocker)
Blocks damage between members of the same Rust team using OnEntityTakeDamage, with a permission whitelist and a /ff toggle for team leaders that persists per team across restarts.
Rust teams have no built-in friendly fire protection, so a stray satchel or a rushed spray can wipe a teammate while the team is raiding. You cannot fix this with a server setting - the only place to stop it is damage resolution.
This plugin implements OnEntityTakeDamage(BasePlayer, HitInfo) and returns a non-null value to cancel the damage when both the victim and the attacker are on the same team. Two config keys shape the behavior: AllowDamageWithPermission lets staff or a donor group keep hitting teammates, and AllowTeamLeaderToggle enables a /ff chat command that the team leader uses to turn friendly fire on and off for that team. Toggled states are written to oxide/data/NoFriendlyFire.json and reloaded on restart, so a team that enabled friendly fire to duel keeps that setting until the leader flips it back.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("No Friendly Fire", "RustPluginGenerator", "1.0.0")]
[Description("Blocks damage between members of the same team, with a per-team toggle.")]
public class NoFriendlyFire : RustPlugin
{
private const string PermUse = "nofriendlyfire.use";
private const string PermBypass = "nofriendlyfire.bypass";
private const string DataFile = "NoFriendlyFire";
private class PluginConfig
{
[JsonProperty("Allow damage with nofriendlyfire.use permission")]
public bool AllowDamageWithPermission = true;
[JsonProperty("Allow team leader toggle")]
public bool AllowTeamLeaderToggle = true;
[JsonProperty("Default state for teams that never toggled (true = friendly fire on)")]
public bool DefaultState = false;
[JsonProperty("Show chat message to victim when hit is blocked")]
public bool ShowBlockMessage = true;
[JsonProperty("Block message cooldown per victim (seconds)")]
public float BlockMessageCooldown = 8f;
}
private PluginConfig config;
private Dictionary<ulong, bool> teamStates = new Dictionary<ulong, bool>();
private readonly Dictionary<ulong, float> messageCooldowns = new Dictionary<ulong, float>();
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>
{
["Blocked"] = "Friendly fire is disabled: {0} cannot damage you.",
["NoTeam"] = "You are not in a team.",
["NotLeader"] = "Only the team leader can change friendly fire.",
["NoPermission"] = "You do not have permission to use this command.",
["ToggleOn"] = "Friendly fire is now ENABLED for your team.",
["ToggleOff"] = "Friendly fire is now DISABLED for your team.",
["Current"] = "Friendly fire is currently {0} for your team.",
["On"] = "enabled",
["Off"] = "disabled",
["Usage"] = "Usage: /ff on|off"
}, 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(PermUse, this);
permission.RegisterPermission(PermBypass, this);
}
private void Loaded()
{
teamStates = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, bool>>(DataFile)
?? new Dictionary<ulong, bool>();
}
private void Unload()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFile, teamStates);
messageCooldowns.Clear();
}
private void SaveStates() => Interface.Oxide.DataFileSystem.WriteObject(DataFile, teamStates);
private bool IsProtected(ulong teamId)
{
bool enabled;
return teamStates.TryGetValue(teamId, out enabled) ? !enabled : !config.DefaultState;
}
private object OnEntityTakeDamage(BasePlayer player, HitInfo hitInfo)
{
if (player == null || hitInfo == null || player.IsNpc) return null;
BasePlayer attacker = hitInfo.InitiatorPlayer != null ? hitInfo.InitiatorPlayer : hitInfo.Initiator as BasePlayer;
if (attacker == null || attacker.IsNpc) return null;
if (player.userID == attacker.userID) return null;
ulong victimTeam = player.currentTeam;
if (victimTeam == 0 || attacker.currentTeam != victimTeam) return null;
string attackerId = attacker.UserIDString;
string victimId = player.UserIDString;
if (permission.UserHasPermission(attackerId, PermBypass) || permission.UserHasPermission(victimId, PermBypass)) return null;
if (config.AllowDamageWithPermission &&
(permission.UserHasPermission(attackerId, PermUse) || permission.UserHasPermission(victimId, PermUse))) return null;
if (!IsProtected(victimTeam)) return null;
if (config.ShowBlockMessage && attacker != player)
{
float now = Time.realtimeSinceStartup;
float last;
if (!messageCooldowns.TryGetValue(player.userID, out last) || now - last >= config.BlockMessageCooldown)
{
messageCooldowns[player.userID] = now;
player.ChatMessage(Msg("Blocked", victimId, attacker.displayName));
}
}
return true;
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player != null) messageCooldowns.Remove(player.userID);
}
[ChatCommand("ff")]
private void CmdFriendlyFire(BasePlayer player, string command, string[] args)
{
if (player == null) return;
string userId = player.UserIDString;
if (!permission.UserHasPermission(userId, PermUse))
{
player.ChatMessage(Msg("NoPermission", userId));
return;
}
if (!config.AllowTeamLeaderToggle)
{
player.ChatMessage(Msg("NoPermission", userId));
return;
}
ulong teamId = player.currentTeam;
if (teamId == 0)
{
player.ChatMessage(Msg("NoTeam", userId));
return;
}
var team = RelationshipManager.ServerInstance.FindTeam(teamId);
if (team == null || team.teamLeader != player.userID)
{
player.ChatMessage(Msg("NotLeader", userId));
return;
}
bool friendlyFireOn = !IsProtected(teamId);
if (args == null || args.Length == 0)
{
player.ChatMessage(Msg("Current", userId, Msg(friendlyFireOn ? "On" : "Off", userId)));
return;
}
string state = args[0].ToLowerInvariant();
if (state != "on" && state != "off")
{
player.ChatMessage(Msg("Usage", userId));
return;
}
bool enable = state == "on";
teamStates[teamId] = enable;
SaveStates();
foreach (ulong memberId in team.members)
{
BasePlayer member = BasePlayer.FindByID(memberId);
if (member == null || !member.IsConnected) continue;
member.ChatMessage(Msg(enable ? "ToggleOn" : "ToggleOff", member.UserIDString));
}
}
}
}Hooks used: OnEntityTakeDamage, OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/NoFriendlyFire.json.
{
"Allow damage with nofriendlyfire.use permission": true,
"Allow team leader toggle": true,
"Default state for teams that never toggled (true = friendly fire on)": false,
"Show chat message to victim when hit is blocked": true,
"Block message cooldown per victim (seconds)": 8
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/ff [on|off] | Team leader toggles friendly fire for the whole team, or reads the current state when no argument is given. | nofriendlyfire.use |
| Permission | Grants |
|---|---|
nofriendlyfire.use | Allows using /ff as a team leader and, when AllowDamageWithPermission is enabled, allows damaging teammates. |
nofriendlyfire.bypass | Always exempt from the block, regardless of the AllowDamageWithPermission config key. |
How the code works
Identify victim and attacker
OnEntityTakeDamage(BasePlayer player, HitInfo hitInfo)only fires for player victims, so NPCs, animals and building parts stay untouched. The hook reads the attacker withhitInfo.InitiatorPlayerfirst and falls back tohitInfo.Initiator as BasePlayer, because some damage sources (like flamethrowers and certain explosives) populateInitiatorbut notInitiatorPlayer. Both objects are null-checked before any team lookup.Compare teams
Damage is blocked only when both players have
currentTeam != 0, both teams are equal, and neither ID is the same player (self-damage like grenades still applies). Everything else returnsnull, which tells Oxide to run the vanilla damage path unchanged. The damage type is not inspected, so hitscan, melee, bleeding ticks and explosions are all covered by the same check.Respect the toggle and permissions
Before blocking, the plugin checks
AllowDamageWithPermissionandpermission.UserHasPermission(id, PermUse)for both players, and separately checksPermBypass. It then asks the storedDictionary<ulong, bool>for the team ID:truemeans friendly fire is enabled, so the hook returnsnull. A single team lookup result is cached in a local variable per call, avoiding duplicateFindTeamwork.The /ff command
Only the player listed as
teamLeaderinRelationshipManager.ServerInstance.FindTeam(...)can change the state, and only with thenofriendlyfire.usepermission. With no argument the command replies with the current setting from lang messages;onandoffwrite the new value, save data and reply to the caller. Players without a team get a chat error and the command exits without touching data.Persistence and the opt-out default
Loaded()readsoxide/data/NoFriendlyFire.jsonthroughInterface.Oxide.DataFileSystem, creating an empty dictionary when the file is missing. InUnload()a timer-driven save is replaced by writing immediately so a restart does not lose a live toggle. TheDefaultStateconfig key decides what teams that never ran/ffget:falsemeans friendly fire is off (protected),truemeans unprotected.Reporting blocked hits (optional)
When
ShowBlockMessageis enabled the victim receives a short chat notice through the lang keyBlocked, with the attacker name inserted withstring.Format. This is throttled by an in-memoryDictionary<ulong, float>ofTime.realtimeSinceStartupvalues so a 600-round spray does not flood chat. Throttle entries are cleared onOnPlayerDisconnectedand inUnload().
Install it on your server
Save NoFriendlyFire.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/NoFriendlyFire.json(Carbon:carbon/configs/NoFriendlyFire.json). After editing, reload it:oxide.reload NoFriendlyFire c.reload NoFriendlyFire # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default nofriendlyfire.use c.grant group default nofriendlyfire.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Only block during raids
Disable the protection while a raid is active so teammates can clear their own base.
Generate this versionTeam damage log to Discord-free file
Record every blocked hit with attacker, victim and position for admin review.
Generate this versionZones where friendly fire is always allowed
Whitelist radius around a monument or event arena.
Generate this version
FAQ
Does this stop damage from turrets, traps or SAM sites owned by a teammate?
Only when the attacker resolves to a player. Turrets, autoturrets, flamethrower traps and landmines have a BaseEntity initiator without an attached player, so hitInfo.InitiatorPlayer is null and the damage proceeds normally. If you also want to stop team-owned traps, you must resolve the entity's OwnerID to a player and compare teams yourself.
Why do rockets sometimes still damage teammates?
Explosions apply damage to nearby entities through a radius query. The exact InitiatorPlayer is set on each hit, so team members are blocked, but the rocket still physically pushes ragdolls and can destroy buildings. If you see a teammate take damage, check that both players actually share a team - the block requires currentTeam to be non-zero and equal on both sides.
Does the /ff setting survive a map wipe?
No. Team IDs are generated per save, so after a wipe or a server.save-less restart the stored IDs point to teams that no longer exist. The plugin keeps the entries (they are harmless) but you can clear oxide/data/NoFriendlyFire.json between wipes to keep the file small.
Will this work on Carbon?
Yes. Carbon loads standard Oxide plugins, and the hook signatures used here are identical in both frameworks. Drop NoFriendlyFire.cs into oxide/plugins, run oxide.reload NoFriendlyFire, and set the permissions with oxide.grant as usual.
What happens for a team that never uses /ff?
The DefaultState config key applies. With the shipped default of false, an unset team is protected; changing it to true means teams must opt in. A team's /ff choice always wins over DefaultState, even after restart.
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
- Headshot announcementsAnnounces headshot kills in chat with killer, victim, weapon and distance, and grants a configurable bonus item when the shot traveled at least aโฆOnServerInitializedOnEntityDeath
Last updated Oct 26, 2026.
Customize friendly fire protection
Customize