How to make a Rust PvP zones plugin (spherical zones, Oxide/Carbon)
Blocks player-versus-player damage everywhere except inside named spherical zones defined in the config, using OnEntityTakeDamage to cancel damage between players outside those zones. Admins add, list and remove zones with console commands.
Most Rust servers want players to fight sometimes and farm safely the rest of the time. A simple way to get that is to allow player damage only inside defined arenas and cancel it everywhere else, so nobody loses gear to a random opponent while running to the next monument. The cleanest implementation point is OnEntityTakeDamage: when both the victim and the attacker are players, check whether the victim stands inside any configured zone and cancel the hit if not. NPC damage, fall damage and structure damage stay untouched.
The plugin below stores zones as a name, a world position and a radius. The /pvpzone add <name> console command drops a zone at your current position, /pvpzone list shows what exists and /pvpzone remove <name> deletes one. Everything is written back to the config file, so zones survive restarts without a database. Players in safe areas get a short chat reminder when their hit is blocked.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("PvpZones", "RustPluginGenerator", "1.0.0")]
[Description("Blocks player-versus-player damage outside configured spherical zones.")]
public class PvpZones : RustPlugin
{
private const string PermAdmin = "pvpzones.admin";
private PluginConfig config;
private readonly Dictionary<ulong, float> lastNotice = new Dictionary<ulong, float>();
private class ZoneEntry
{
[JsonProperty("Name")]
public string Name { get; set; }
[JsonProperty("X")]
public float X { get; set; }
[JsonProperty("Y")]
public float Y { get; set; }
[JsonProperty("Z")]
public float Z { get; set; }
[JsonProperty("Radius")]
public float Radius { get; set; }
[JsonIgnore]
public Vector3 Center => new Vector3(X, Y, Z);
}
private class PluginConfig
{
[JsonProperty("Default radius")]
public float DefaultRadius { get; set; } = 50f;
[JsonProperty("Seconds between blocked-hit notices")]
public float NoticeCooldown { get; set; } = 10f;
[JsonProperty("Zones")]
public List<ZoneEntry> Zones { get; set; } = new List<ZoneEntry>();
}
protected override void LoadDefaultConfig() => config = new PluginConfig();
protected override void LoadConfig()
{
base.LoadConfig();
try
{
config = Config.ReadObject<PluginConfig>();
if (config == null) throw new Exception();
if (config.Zones == null) config.Zones = new List<ZoneEntry>();
}
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>
{
["NoPvpZone"] = "PvP is disabled here. Fight inside a marked arena.",
["ZoneAdded"] = "Zone '{0}' created at {1} with radius {2}.",
["ZoneRemoved"] = "Zone '{0}' removed.",
["ZoneMissing"] = "No zone named '{0}'.",
["NoZones"] = "No PvP zones are configured.",
["ZoneList"] = "{0} - {1} r{2}",
["NeedName"] = "Usage: pvpzone add <name> | pvpzone remove <name> | pvpzone list",
["NoPermission"] = "You do not have permission to use this command.",
["PlayerOnly"] = "This command must be run in game from a player."
}, 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(PermAdmin, this);
}
private object OnEntityTakeDamage(BasePlayer player, HitInfo hitInfo)
{
if (player == null || hitInfo == null) return null;
var attacker = hitInfo.InitiatorPlayer;
if (attacker == null || attacker == player) return null;
if (attacker.IsNpc || player.IsNpc) return null;
if (IsInsideAnyZone(player.transform.position)) return null;
hitInfo.damageTypes.ScaleAll(0f);
if (config.NoticeCooldown > 0f)
{
float now = UnityEngine.Time.realtimeSinceStartup;
float last;
if (!lastNotice.TryGetValue(player.userID, out last) || now - last >= config.NoticeCooldown)
{
lastNotice[player.userID] = now;
player.ChatMessage(Msg("NoPvpZone", player.UserIDString));
}
}
return true;
}
private bool IsInsideAnyZone(Vector3 position)
{
for (int i = 0; i < config.Zones.Count; i++)
{
var zone = config.Zones[i];
if (zone == null || zone.Radius <= 0f) continue;
if (Vector3.Distance(position, zone.Center) <= zone.Radius) return true;
}
return false;
}
[ConsoleCommand("pvpzone")]
private void CmdPvpZone(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
string sub = arg.GetString(0, string.Empty).ToLowerInvariant();
if (sub == "add")
{
if (player == null)
{
arg.ReplyWith(Msg("PlayerOnly"));
return;
}
string name = arg.GetString(1, string.Empty);
if (string.IsNullOrEmpty(name))
{
arg.ReplyWith(Msg("NeedName"));
return;
}
if (FindZone(name) != null)
{
arg.ReplyWith(Msg("ZoneMissing", null, name));
return;
}
var pos = player.transform.position;
var zone = new ZoneEntry { Name = name, X = pos.x, Y = pos.y, Z = pos.z, Radius = config.DefaultRadius };
config.Zones.Add(zone);
SaveConfig();
arg.ReplyWith(Msg("ZoneAdded", null, zone.Name, pos.ToString("F0"), zone.Radius));
}
else if (sub == "remove")
{
string name = arg.GetString(1, string.Empty);
var zone = FindZone(name);
if (zone == null)
{
arg.ReplyWith(Msg("ZoneMissing", null, name));
return;
}
config.Zones.Remove(zone);
SaveConfig();
arg.ReplyWith(Msg("ZoneRemoved", null, zone.Name));
}
else if (sub == "list")
{
if (config.Zones.Count == 0)
{
arg.ReplyWith(Msg("NoZones"));
return;
}
for (int i = 0; i < config.Zones.Count; i++)
{
var zone = config.Zones[i];
arg.ReplyWith(Msg("ZoneList", null, zone.Name, zone.Center.ToString("F0"), zone.Radius));
}
}
else
{
arg.ReplyWith(Msg("NeedName"));
}
}
private ZoneEntry FindZone(string name)
{
if (string.IsNullOrEmpty(name)) return null;
for (int i = 0; i < config.Zones.Count; i++)
{
if (string.Equals(config.Zones[i].Name, name, StringComparison.OrdinalIgnoreCase)) return config.Zones[i];
}
return null;
}
private void Unload()
{
SaveConfig();
lastNotice.Clear();
}
}
}Hooks used: OnEntityTakeDamage
Config file
Created automatically on first load at oxide/config/PvpZones.json.
{
"Default radius": 50,
"Seconds between blocked-hit notices": 10,
"Zones": [
{
"Name": "Example",
"X": 0,
"Y": 0,
"Z": 0,
"Radius": 50
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
pvpzone add <name> | Creates a spherical PvP zone at your current position using the configured default radius and saves it to the config. | pvpzones.admin |
pvpzone remove <name> | Deletes the named zone from the config. | pvpzones.admin |
pvpzone list | Lists every zone with its position and radius. | pvpzones.admin |
| Permission | Grants |
|---|---|
pvpzones.admin | Grants access to the pvpzone console command (add, remove, list). |
How the code works
Config shape for zones
The config class holds a
List<ZoneEntry>where each entry hasName,X,Y,ZandRadius. Keeping plain floats instead of aVector3means the JSON stays readable and you can hand-edit coordinates without a serializer quirk.LoadConfigfalls back toLoadDefaultConfigwhen the file cannot be parsed, so a broken edit never stops the plugin from starting.SaveConfigwrites the list back after every add or remove.The damage hook
object OnEntityTakeDamage(BasePlayer player, HitInfo hitInfo)fires for players only. Read the attacker fromhitInfo.InitiatorPlayerand bail out when it is null, when the attacker is the victim, or when either side is an NPC. Then callIsInsideAnyZone(player.transform.position). If the answer is false, sethitInfo.damageTypes.ScaleAll(0f)and returntrueto mark the damage handled. Returningnulllets normal damage through inside zones.Why the more specific overload is enough
Oxide picks hook methods by checking that every argument matches the declared parameter types. Declaring
BasePlayermeans the plugin is only called when the victim is a player, so animals, deployables and building blocks keep vanilla behavior without extra type checks. If you later want to protect NPCs or consoles too, add another overload forBaseCombatEntity— but the current setup keeps the plugin focused and cheap on large servers.Point-in-sphere test
IsInsideAnyZoneloops over the cached zone list and comparesVector3.Distance(position, center)againstRadius. Distance is squared internally by Unity, so a few dozen zones cost almost nothing per hit. Keep the list in a field rebuilt after load and after each command rather than reading the config object on every hit.Admin console command
The single
[ConsoleCommand("pvpzone")]handler readsarg.GetString(0, "")for the subcommand.addrequires a name, pulls the caller fromarg.Player()(null on RCON, so reply with a message), builds a newZoneEntryfromplayer.transform.positionand the configuredDefaultRadius, then saves. Because it is a console command it works from the game console or RCON; the permission check viapermission.UserHasPermissionkeeps random players from using it when they are admins on another system.Player feedback
When a hit is blocked the victim receives one localized message per cooldown window, tracked in a
Dictionary<ulong, float>of last-notice timestamps and compared againstUnityEngine.Time.realtimeSinceStartup. Without that throttle a firefight in a safe area would spam chat. All text lives inLoadDefaultMessages, so servers with a non-English player base can translateNoPvpZoneand the admin replies in the language files.
Install it on your server
Save PvpZones.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/PvpZones.json(Carbon:carbon/configs/PvpZones.json). After editing, reload it:oxide.reload PvpZones c.reload PvpZones # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default pvpzones.admin c.grant group default pvpzones.admin # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Show zones on the map
Draw every configured zone as a circle in the in-game map so players can find the arenas.
Generate this versionCylindrical zones
Ignore the Y axis so zones cover full columns of terrain and buildings.
Generate this versionGather multiplier inside zones
Give a resource bonus inside PvP zones to reward risk.
Generate this version
FAQ
Does the plugin block NPC or animal damage?
No. Only hits between two players are evaluated, because the hook overload declares BasePlayer and the plugin returns early when hitInfo.InitiatorPlayer is null. Scientists, bears and Bradley damage work exactly as in vanilla, so PvE content stays dangerous everywhere.
Where should I place the plugin file?
Drop PvpZones.cs into oxide/plugins and wait for Oxide or Carbon to compile it. The config is written to oxide/config/PvpZones.json on first load. Change a zone with the pvpzone console command from the in-game F1 console or through RCON.
How do I find coordinates for a zone?
Stand where you want the center and run pvpzone add <name> — it records your current position. Radius comes from Default Radius in the config; edit the JSON afterwards if you need a larger or smaller arena, or add multiple entries by hand.
What happens to vehicles and raid damage inside safe areas?
The plugin only cancels damage where the victim is a player, so rockets fired at a base outside a zone still destroy it. A player standing in the safe area while shooting out is protected from return fire; if you need the opposite behavior, remove the zone check from the attacker side or add a distance check.
Does this conflict with other damage plugins?
Multiple plugins can subscribe to OnEntityTakeDamage; Oxide runs them in load order and the first non-null return cancels the rest. If a raid or god-mode plugin runs before PvpZones it can override the cancellation. Load order can be adjusted by renaming files or using a permission/plugin hook to coordinate.
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 zones
Customize