Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/ff [on|off]Team leader toggles friendly fire for the whole team, or reads the current state when no argument is given.nofriendlyfire.use
Permissions
PermissionGrants
nofriendlyfire.useAllows using /ff as a team leader and, when AllowDamageWithPermission is enabled, allows damaging teammates.
nofriendlyfire.bypassAlways exempt from the block, regardless of the AllowDamageWithPermission config key.

How the code works

  1. 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 with hitInfo.InitiatorPlayer first and falls back to hitInfo.Initiator as BasePlayer, because some damage sources (like flamethrowers and certain explosives) populate Initiator but not InitiatorPlayer. Both objects are null-checked before any team lookup.

  2. 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 returns null, 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.

  3. Respect the toggle and permissions

    Before blocking, the plugin checks AllowDamageWithPermission and permission.UserHasPermission(id, PermUse) for both players, and separately checks PermBypass. It then asks the stored Dictionary<ulong, bool> for the team ID: true means friendly fire is enabled, so the hook returns null. A single team lookup result is cached in a local variable per call, avoiding duplicate FindTeam work.

  4. The /ff command

    Only the player listed as teamLeader in RelationshipManager.ServerInstance.FindTeam(...) can change the state, and only with the nofriendlyfire.use permission. With no argument the command replies with the current setting from lang messages; on and off write 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.

  5. Persistence and the opt-out default

    Loaded() reads oxide/data/NoFriendlyFire.json through Interface.Oxide.DataFileSystem, creating an empty dictionary when the file is missing. In Unload() a timer-driven save is replaced by writing immediately so a restart does not lose a live toggle. The DefaultState config key decides what teams that never ran /ff get: false means friendly fire is off (protected), true means unprotected.

  6. Reporting blocked hits (optional)

    When ShowBlockMessage is enabled the victim receives a short chat notice through the lang key Blocked, with the attacker name inserted with string.Format. This is throttled by an in-memory Dictionary<ulong, float> of Time.realtimeSinceStartup values so a 600-round spray does not flood chat. Throttle entries are cleared on OnPlayerDisconnected and in Unload().

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Team damage log to Discord-free file

    Record every blocked hit with attacker, victim and position for admin review.

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

Prompt: Rust Oxide plugin NoFriendlyFire: block damage between teammates in OnEntityTakeDamage, permission nofriendlyfire.use for /ff and damage whitelist, nofriendlyfire.bypass for staff, persist team toggles to oxide/data.
Customize for my server

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

Last updated Oct 26, 2026.

Customize friendly fire protection

Customize