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
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.
{
"Default PvP Mode": true,
"Toggle Cooldown (seconds)": 300,
"Block Damage Message": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/pvp | Toggle your PvP mode. Without an argument it flips the current value and tells you the new state or the remaining cooldown. | Everyone |
/pvp status | Show 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 |
| Permission | Grants |
|---|---|
pvptoggle.use | Grants access to the /pvp chat command. |
pvptoggle.admin | Grants access to the pvp.toggle console command for changing other players. |
pvptoggle.nocooldown | Skips the configured cooldown when toggling. |
How the code works
The config class and data file
PluginConfigholds the defaults:Default PvP Modesets what a player who has never run the command gets,Toggle Cooldown (seconds)sets the wait, andBlock Damage Messagecontrols whether the victim is told why the hit did nothing. Player state lives in a separateStoredDataobject saved throughInterface.Oxide.DataFileSystemunder the plugin name, so it survives restarts and map wipes.LoadConfigand the data load both run inOnServerInitialized, where the plugin also builds a dictionary keyed by Steam ID for fast lookups.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 inheritDefault PvP Modewithout any hook having to write rows on connect. The/pvp statuspath and the damage check both call the same method.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 comparesDateTime.UtcNowagainst the player'sLastToggletimestamp in the data file and refuses if the cooldown has not elapsed. Players with thepvptoggle.nocooldownpermission bypass that check.statusonly prints the current mode. Every reply goes through theMsghelper andlangkeys.Blocking damage between mismatched modes
The
OnEntityTakeDamage(BasePlayer, HitInfo)overload is called by Oxide only when the hurt entity is aBasePlayer, so no type casting is needed. The method pulls the attacker fromhitInfo.InitiatorPlayer, returns null for self-damage or non-player sources, then comparesIsPvpOnfor both sides. If either is off it callshitInfo.damageTypes.ScaleAll(0f), returnstrueto signal that vanilla handling has been overridden, and optionally messages the victim. Returningnullin every other case leaves normal PvP alone.The admin console command
pvp.toggle <name or steamid>is a[ConsoleCommand]without a leading slash. It resolves the target withBasePlayer.FindAwakeOrSleeping, checkspermission.UserHasPermissionforpvptoggle.adminwhen the caller is a player, flips the stored value and saves. RCON and the server console have no player, soarg.Player()returns null and the permission check is skipped.Saving and unloading
The dictionary is written to disk on
OnServerSaveand again inUnload, so a crash loses at most one save interval.Unloadwrites 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:OnEntityTakeDamagecan fire hundreds of times per second on a busy server.
Install it on your server
Save PvpToggle.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/PvpToggle.json(Carbon:carbon/configs/PvpToggle.json). After editing, reload it:oxide.reload PvpToggle c.reload PvpToggle # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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.
Force PvP zones
Ignore the toggle inside configured monument or event zones.
Generate this versionTimed PvP windows
Automatically turn PvP on for everyone during configured hours.
Generate this versionPvP-only loot
Give a bonus when a player kills someone in PvP mode.
Generate this version
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.
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
- Friendly fire protectionBlocks damage between members of the same Rust team using OnEntityTakeDamage, with a permission whitelist and a /ff toggle for team leaders thatโฆOnEntityTakeDamageOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize pvp toggle
Customize