Skip to content
RustPluginGenerator

How to make a Rust PvP toggle plugin (Oxide/Carbon)

Adds a /pvp command that switches each player between PvP and PvE mode with a per-player cooldown, stores the choice in oxide/data, and blocks player-versus-player damage unless both sides have PvP enabled. It uses OnEntityTakeDamage(BasePlayer, HitInfo) to cancel damage.

Servers that mix builders and fighters usually want a consent flag: players who never want to be shot should be able to opt out without leaving the server. This recipe builds that flag yourself. Each player has a boolean PvP mode persisted in a data file under oxide/data, a default starting value from the config, and a cooldown that stops people flipping the switch mid-fight to dodge a bullet. The OnEntityTakeDamage overload that targets BasePlayer is where the rule is enforced: when either the victim or the attacker has PvP off, the damage event is cancelled before armor or health are touched. One chat command, /pvp, reads or changes the mode, and an optional admin command reports any player's current state. All player-facing text goes through the language file so you can rebrand it.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("PvpToggle", "RustPluginGenerator", "1.0.0")]
    [Description("Lets players opt in or out of PvP damage with a cooldown and persists their choice.")]
    public class PvpToggle : RustPlugin
    {
        private const string PermUse = "pvptoggle.use";
        private const string PermAdmin = "pvptoggle.admin";
        private const string PermNoCooldown = "pvptoggle.nocooldown";

        private PluginConfig config;
        private StoredData data;

        public class PluginConfig
        {
            [JsonProperty("Default PvP Mode")]
            public bool DefaultPvpMode = true;

            [JsonProperty("Toggle Cooldown (seconds)")]
            public float ToggleCooldown = 300f;

            [JsonProperty("Block Damage Message")]
            public bool BlockDamageMessage = true;
        }

        public class StoredData
        {
            [JsonProperty("Players")]
            public Dictionary<ulong, PlayerState> Players = new Dictionary<ulong, PlayerState>();
        }

        public class PlayerState
        {
            [JsonProperty("PvP Enabled")]
            public bool PvpEnabled;

            [JsonProperty("Last Toggle (UTC)")]
            public DateTime LastToggle = DateTime.MinValue;
        }

        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>
            {
                ["Toggle.Enabled"] = "PvP mode is now <color=#ff4444>ON</color>. Other players can hurt you and you can hurt them.",
                ["Toggle.Disabled"] = "PvP mode is now <color=#44ff44>OFF</color>. You are protected from player damage and deal none.",
                ["Toggle.Cooldown"] = "You must wait {0} more seconds before changing your PvP mode.",
                ["Status.On"] = "Your PvP mode is currently ON.",
                ["Status.Off"] = "Your PvP mode is currently OFF.",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["BlockedDamage"] = "Damage blocked: you or your attacker has PvP mode disabled.",
                ["Admin.Toggled"] = "PvP mode for {0} is now {1}.",
                ["Admin.NotFound"] = "No player found matching that name or ID."
            }, 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(PermAdmin, this);
            permission.RegisterPermission(PermNoCooldown, this);
        }

        private void OnServerInitialized()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name) ?? new StoredData();
        }

        private void OnServerSave()
        {
            if (data != null) Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        private void Unload()
        {
            if (data != null) Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        private bool IsPvpOn(ulong userId)
        {
            PlayerState state;
            if (data != null && data.Players.TryGetValue(userId, out state))
                return state.PvpEnabled;
            return config.DefaultPvpMode;
        }

        private PlayerState GetOrCreateState(ulong userId)
        {
            PlayerState state;
            if (!data.Players.TryGetValue(userId, out state))
            {
                state = new PlayerState { PvpEnabled = config.DefaultPvpMode, LastToggle = DateTime.MinValue };
                data.Players[userId] = state;
            }
            return state;
        }

        [ChatCommand("pvp")]
        private void CmdPvp(BasePlayer player, string command, string[] args)
        {
            if (player == null || !player.IsConnected) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args != null && args.Length > 0 && args[0].Equals("status", StringComparison.OrdinalIgnoreCase))
            {
                player.ChatMessage(Msg(IsPvpOn(player.userID.Get()) ? "Status.On" : "Status.Off", player.UserIDString));
                return;
            }

            var state = GetOrCreateState(player.userID.Get());

            if (!permission.UserHasPermission(player.UserIDString, PermNoCooldown) && config.ToggleCooldown > 0f)
            {
                double elapsed = (DateTime.UtcNow - state.LastToggle).TotalSeconds;
                if (elapsed < config.ToggleCooldown)
                {
                    player.ChatMessage(Msg("Toggle.Cooldown", player.UserIDString, Math.Ceiling(config.ToggleCooldown - elapsed)));
                    return;
                }
            }

            state.PvpEnabled = !state.PvpEnabled;
            state.LastToggle = DateTime.UtcNow;
            player.ChatMessage(Msg(state.PvpEnabled ? "Toggle.Enabled" : "Toggle.Disabled", player.UserIDString));
            Interface.Oxide.DataFileSystem.WriteObject(Name, data);
        }

        [ConsoleCommand("pvp.toggle")]
        private void CcmdPvpToggle(ConsoleSystem.Arg arg)
        {
            var caller = arg.Player();
            if (caller != null && !permission.UserHasPermission(caller.UserIDString, PermAdmin))
            {
                arg.ReplyWith(Msg("NoPermission", caller.UserIDString));
                return;
            }

            string target = arg.GetString(0, string.Empty);
            if (string.IsNullOrEmpty(target))
            {
                arg.ReplyWith("Usage: pvp.toggle <name or steamid>");
                return;
            }

            var player = BasePlayer.FindAwakeOrSleeping(target);
            if (player == null)
            {
                arg.ReplyWith(Msg("Admin.NotFound", caller == null ? null : caller.UserIDString));
                return;
            }

            var state = GetOrCreateState(player.userID.Get());
            state.PvpEnabled = !state.PvpEnabled;
            state.LastToggle = DateTime.UtcNow;
            Interface.Oxide.DataFileSystem.WriteObject(Name, data);

            arg.ReplyWith(Msg("Admin.Toggled", caller == null ? null : caller.UserIDString, player.displayName, state.PvpEnabled ? "ON" : "OFF"));
            player.ChatMessage(Msg(state.PvpEnabled ? "Toggle.Enabled" : "Toggle.Disabled", player.UserIDString));
        }

        private object OnEntityTakeDamage(BasePlayer player, HitInfo hitInfo)
        {
            if (player == null || hitInfo == null) return null;

            var attacker = hitInfo.InitiatorPlayer;
            if (attacker == null || attacker == player || attacker.IsNpc) return null;

            if (IsPvpOn(player.userID.Get()) && IsPvpOn(attacker.userID.Get())) return null;

            hitInfo.damageTypes.ScaleAll(0f);

            if (config.BlockDamageMessage)
                player.ChatMessage(Msg("BlockedDamage", player.UserIDString));

            return true;
        }
    }
}

Hooks used: OnServerInitialized, OnServerSave, OnEntityTakeDamage

Config file

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

PvpToggle.json
{
  "Default PvP Mode": true,
  "Toggle Cooldown (seconds)": 300,
  "Block Damage Message": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/pvpToggle your PvP mode. Without an argument it flips the current value and tells you the new state or the remaining cooldown.Everyone
/pvp statusShow your current PvP mode without changing it.Everyone
pvp.toggle <name or steamid>Admin command that flips another player's PvP mode, ignoring the default-mode logic.pvptoggle.admin
Permissions
PermissionGrants
pvptoggle.useGrants access to the /pvp chat command.
pvptoggle.adminGrants access to the pvp.toggle console command for changing other players.
pvptoggle.nocooldownSkips the configured cooldown when toggling.

How the code works

  1. The config class and data file

    PluginConfig holds the defaults: Default PvP Mode sets what a player who has never run the command gets, Toggle Cooldown (seconds) sets the wait, and Block Damage Message controls whether the victim is told why the hit did nothing. Player state lives in a separate StoredData object saved through Interface.Oxide.DataFileSystem under the plugin name, so it survives restarts and map wipes. LoadConfig and the data load both run in OnServerInitialized, where the plugin also builds a dictionary keyed by Steam ID for fast lookups.

  2. Resolving a player's mode

    A single helper, IsPvpOn(ulong userId), answers every question: it looks the ID up in the stored dictionary and returns the saved flag, or the configured default when the entry is missing. Keeping this logic in one place means new joiners automatically inherit Default PvP Mode without any hook having to write rows on connect. The /pvp status path and the damage check both call the same method.

  3. Handling the chat command

    [ChatCommand("pvp")] receives the player, the command and the argument array. With no arguments the command flips the flag, but first compares DateTime.UtcNow against the player's LastToggle timestamp in the data file and refuses if the cooldown has not elapsed. Players with the pvptoggle.nocooldown permission bypass that check. status only prints the current mode. Every reply goes through the Msg helper and lang keys.

  4. Blocking damage between mismatched modes

    The OnEntityTakeDamage(BasePlayer, HitInfo) overload is called by Oxide only when the hurt entity is a BasePlayer, so no type casting is needed. The method pulls the attacker from hitInfo.InitiatorPlayer, returns null for self-damage or non-player sources, then compares IsPvpOn for both sides. If either is off it calls hitInfo.damageTypes.ScaleAll(0f), returns true to signal that vanilla handling has been overridden, and optionally messages the victim. Returning null in every other case leaves normal PvP alone.

  5. The admin console command

    pvp.toggle <name or steamid> is a [ConsoleCommand] without a leading slash. It resolves the target with BasePlayer.FindAwakeOrSleeping, checks permission.UserHasPermission for pvptoggle.admin when the caller is a player, flips the stored value and saves. RCON and the server console have no player, so arg.Player() returns null and the permission check is skipped.

  6. Saving and unloading

    The dictionary is written to disk on OnServerSave and again in Unload, so a crash loses at most one save interval. Unload writes the data file, unregisters nothing else (Oxide handles that) and leaves the timers list empty because this plugin uses no repeating timers. Keeping writes out of the damage hook matters: OnEntityTakeDamage can fire hundreds of times per second on a busy server.

Install it on your server

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

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

Variations

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

FAQ

Does a player with PvP off become invincible?

No. The plugin only cancels damage whose hitInfo.InitiatorPlayer is a real player. NPCs, animals, airdrops, Bradley and helicopters are untouched, and environmental damage still applies. If you want PvE players to also be safe from NPCs that would be a separate rule with its own tradeoff.

What happens when a PvP-off player shoots someone?

The check is symmetric: if either the attacker or the victim has PvP off, the damage is scaled to zero. That stops players from hiding behind the flag as a free shield while attacking. The victim sees the Blocked Damage message when Block Damage Message is on.

Where is the state stored and does it survive a wipe?

Player modes live in a data file under oxide/data keyed by the plugin name, written on server save and on unload. Data files are not removed by a map wipe, so modes persist unless you delete the file or run oxide.reload PvpToggle with your own reset logic.

Can the cooldown be bypassed?

Only by players holding pvptoggle.nocooldown, which you should grant sparingly. The cooldown timestamp is saved alongside the mode, so reconnecting does not reset it. Set Toggle Cooldown (seconds) to 0 to remove the restriction entirely.

How do I install this on Oxide or Carbon?

Drop the .cs file into oxide/plugins, then run oxide.reload PvpToggle or restart. Carbon loads the same file from its own plugins folder. Edit oxide/config/PvpToggle.json and reload to change the default mode or cooldown; existing players keep their saved choice.

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 with a /pvp command that toggles PvP mode per player, stores it in oxide/data, blocks player-versus-player damage unless both have PvP on, and has a configurable cooldown and default mode.
Customize for my server

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

Last updated Oct 26, 2026.

Customize pvp toggle

Customize