How to make a Rust no-NPC zones plugin (remove scientists)
Defines named world zones where newly spawned scientists and animals are deleted immediately. It uses the OnEntitySpawned hook to check each new entity against the configured zones.
If you run a safe zone, a spawn area or a building event, wandering scientists and wildlife can break the experience. Rust's NPC spawning is handled by the server and there is no built-in way to fence off a region, so a plugin has to intervene the moment an NPC exists. The hook OnEntitySpawned fires for every new networked entity, which gives you a chance to remove the ones that appeared inside your zones before players ever notice them.
This plugin stores a list of zones in its config, each one a horizontal X/Z position and a radius, with an option to ignore height so a zone covers everything above and below it. When a scientist NPC or an animal spawns inside a zone, the entity is killed at once. A single chat command lets admins add, list and remove zones without editing the JSON by hand.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("No NPC Zones", "RustPluginGenerator", "1.0.0")]
[Description("Removes scientists and animals that spawn inside configured world zones.")]
public class NoNpcZones : RustPlugin
{
private const string PermUse = "nonpczones.use";
private const float MaxRadius = 1000f;
private PluginConfig config;
private class ZoneEntry
{
[JsonProperty("Name")]
public string Name;
[JsonProperty("X")]
public float X;
[JsonProperty("Z")]
public float Z;
[JsonProperty("Radius")]
public float Radius;
[JsonProperty("IgnoreHeight")]
public bool IgnoreHeight;
}
private class PluginConfig
{
[JsonProperty("SkipPlayerOwned")]
public bool SkipPlayerOwned = true;
[JsonProperty("Zones")]
public List<ZoneEntry> Zones = 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();
}
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>
{
["ZoneAdded"] = "Zone '{0}' added at X {1:0} Z {2:0} with radius {3:0}.",
["ZoneRemoved"] = "Zone '{0}' removed.",
["ZoneMissing"] = "No zone named '{0}'.",
["ZoneDuplicate"] = "A zone named '{0}' already exists.",
["RadiusInvalid"] = "Radius must be between 1 and {0}.",
["UsageAdd"] = "Usage: /npc nzadd <name> <radius>",
["UsageRemove"] = "Usage: /npc nzremove <name>",
["NoZones"] = "No zones configured.",
["ZoneLine"] = "{0}: X {1:0} Z {2:0} radius {3:0}, ignore height: {4}",
["PlayerOnly"] = "This command can only be used in game."
}, 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);
cmd.AddChatCommand("npc", this, nameof(CmdNpc));
}
private void CmdNpc(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("PlayerOnly", player.UserIDString));
return;
}
if (args == null || args.Length == 0)
{
SendReply(player, Msg("UsageAdd", player.UserIDString));
return;
}
switch (args[0].ToLowerInvariant())
{
case "nzadd":
AddZone(player, args);
break;
case "nzlist":
ListZones(player);
break;
case "nzremove":
RemoveZone(player, args);
break;
default:
SendReply(player, Msg("UsageAdd", player.UserIDString));
break;
}
}
private void AddZone(BasePlayer player, string[] args)
{
if (args.Length < 3)
{
SendReply(player, Msg("UsageAdd", player.UserIDString));
return;
}
string name = args[1].Trim();
if (string.IsNullOrEmpty(name) || FindZone(name) != null)
{
SendReply(player, Msg("ZoneDuplicate", player.UserIDString, name));
return;
}
float radius;
if (!float.TryParse(args[2], out radius) || radius < 1f || radius > MaxRadius)
{
SendReply(player, Msg("RadiusInvalid", player.UserIDString, MaxRadius));
return;
}
Vector3 pos = player.transform.position;
var zone = new ZoneEntry
{
Name = name,
X = pos.x,
Z = pos.z,
Radius = radius,
IgnoreHeight = true
};
config.Zones.Add(zone);
SaveConfig();
SendReply(player, Msg("ZoneAdded", player.UserIDString, zone.Name, zone.X, zone.Z, zone.Radius));
}
private void ListZones(BasePlayer player)
{
if (config.Zones == null || config.Zones.Count == 0)
{
SendReply(player, Msg("NoZones", player.UserIDString));
return;
}
foreach (var zone in config.Zones)
{
SendReply(player, Msg("ZoneLine", player.UserIDString, zone.Name, zone.X, zone.Z, zone.Radius, zone.IgnoreHeight));
}
}
private void RemoveZone(BasePlayer player, string[] args)
{
if (args.Length < 2)
{
SendReply(player, Msg("UsageRemove", player.UserIDString));
return;
}
string name = args[1].Trim();
var zone = FindZone(name);
if (zone == null)
{
SendReply(player, Msg("ZoneMissing", player.UserIDString, name));
return;
}
config.Zones.Remove(zone);
SaveConfig();
SendReply(player, Msg("ZoneRemoved", player.UserIDString, zone.Name));
}
private ZoneEntry FindZone(string name)
{
if (config.Zones == null) return null;
foreach (var zone in config.Zones)
{
if (zone != null && string.Equals(zone.Name, name, StringComparison.OrdinalIgnoreCase))
return zone;
}
return null;
}
private void OnEntitySpawned(BaseNetworkable entity)
{
if (entity == null || config.Zones == null || config.Zones.Count == 0) return;
BaseCombatEntity combat = entity as BaseCombatEntity;
if (combat == null) return;
bool isNpc = entity is ScientistNPC || entity is BaseAnimalNPC;
if (!isNpc) return;
if (config.SkipPlayerOwned && combat.OwnerID != 0) return;
Vector3 pos = combat.transform.position;
for (int i = 0; i < config.Zones.Count; i++)
{
var zone = config.Zones[i];
if (zone == null) continue;
if (IsInside(zone, pos))
{
combat.Kill();
return;
}
}
}
private static bool IsInside(ZoneEntry zone, Vector3 pos)
{
float dx = pos.x - zone.X;
float dz = pos.z - zone.Z;
float flatSq = dx * dx + dz * dz;
if (flatSq > zone.Radius * zone.Radius) return false;
if (zone.IgnoreHeight) return true;
float dy = pos.y;
return flatSq + dy * dy <= zone.Radius * zone.Radius;
}
}
}Hooks used: OnEntitySpawned
Config file
Created automatically on first load at oxide/config/NoNpcZones.json.
{
"SkipPlayerOwned": true,
"Zones": [
{
"Name": "spawn",
"X": 0,
"Z": 0,
"Radius": 150,
"IgnoreHeight": true
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/npc nzadd <name> <radius> | Creates a new zone centered on your current position. | nonpczones.use |
/npc nzlist | Lists all configured zones in chat. | nonpczones.use |
/npc nzremove <name> | Removes the zone with the given name. | nonpczones.use |
| Permission | Grants |
|---|---|
nonpczones.use | Allows using the /npc command to manage zones. |
How the code works
Hook the spawn event and filter entity types
OnEntitySpawned(BaseNetworkable entity)runs for every newly spawned entity, including deployables and projectiles, so the first job is a cheap type check. The code first casts toBaseCombatEntityand returns if that fails, then accepts only entities that areScientistNPCorBaseAnimalNPC. Everything else returns immediately. That keeps the hook light because it is called very often on a busy server, and both checks use theisoperator so subclass instances still match.Read the zone list from config
Each zone is a
ZoneEntrywith aName,X,Z,RadiusandIgnoreHeight. The config class holds aList<ZoneEntry>under the JSON keyZones. On load,ReadObject<PluginConfig>()fills the list andSaveConfig()writes the defaults back if the file was missing or invalid. No data file is needed because the zones are static admin configuration, not player data.Match a position against a zone
The
IsInsidehelper builds a flat distance between the entity'stransform.positionand the zone center usingdxanddz. WhenIgnoreHeightis true, only the X and Z difference is used, so a tall zone (a dome, a tower, a whole grid square) is covered at any elevation. Otherwise the code also compares the Y difference. If the combined squared distance stays withinRadiussquared, the entity is inside the zone and the plugin callsKill()on it.Ignore player-owned NPCs
Before deleting anything, the plugin checks
combat.OwnerID. Player-spawned or player-owned NPCs (for example an admin placing a scientist for an event) have a non-zero owner and are skipped. Built-in world spawns haveOwnerID == 0. The config has aSkipPlayerOwnedtoggle, and the check is only applied when that setting is true, so admins who want every NPC removed regardless of owner can turn it off.Expose a command for zone management
The
/npcchat command is registered in theInitmethod withcmd.AddChatCommandand checked against thenonpczones.usepermission so only trusted staff can edit the zone list. Its first argument selects the action:nzadd <name> <radius>reads the caller's position, adds a zone and saves the config;nzlistprints the current zones in chat;nzremove <name>deletes one by name. The radius must be between 1 and 1000 meters and duplicate names are rejected.Keep the hook fast and safe
OnEntitySpawnedruns on the main thread during spawning, so the plugin does linear work over the zone list only after matching an NPC type. There is no logging inside the hook because the message would spam the console every time an NPC is culled. The plugin has no timers to clean up; the only state is the config object, which is written back when a command changes it.
Install it on your server
Save NoNpcZones.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/NoNpcZones.json(Carbon:carbon/configs/NoNpcZones.json). After editing, reload it:oxide.reload NoNpcZones c.reload NoNpcZones # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default nonpczones.use c.grant group default nonpczones.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Exempt specific prefabs
Allow listed NPC prefabs (for example boss spawns) to spawn even inside a zone.
Generate this versionZone-based entity cap
Limit how many NPCs may exist inside a zone instead of deleting all of them.
Generate this versionRemove vehicles from a zone
Also remove patrol helicopters, Bradley APC and supply drops when they spawn inside a designated zone.
Generate this version
FAQ
Will this stop NPCs that already exist when the plugin loads?
No. The plugin only acts on the OnEntitySpawned hook, so NPCs that spawned before the plugin loaded or before you added a zone will keep existing. Delete them manually with entity.kill via the server console or reload the plugin after a wipe, when the world spawns NPCs fresh.
How do I install this on Oxide or Carbon?
Drop the compiled .cs file into oxide/plugins/. Oxide compiles it on load and creates oxide/config/NoNpcZones.json. Carbon loads the same file. Grant nonpczones.use to admin groups with oxide.grant group admin nonpczones.use and reload the plugin.
Why does my zone not cover high ground?
Each zone defaults to IgnoreHeight: true, which compares only X and Z distance. If you need a spherical check, set IgnoreHeight to false in the JSON for that zone and reload the plugin. The radius then measures distance in all three dimensions.
Can this be used to clear animals on the whole map?
Yes, indirectly. You can set one large zone with Radius around 1000 and IgnoreHeight: true centered on the map; anything further than the radius will still spawn normally. A whole-map setting is better handled with server.conanimaltrue if you want no animals at all.
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 npcs plugins
- Animal spawn commandAdds a permission-gated /animal <species> [count] chat command that spawns vanilla animals in front of the player, with a per-command cap, aโฆ
- Custom NPC lootRewrites the items inside corpses left by configured NPCs, using a weighted per-prefab loot table.OnServerInitializedOnCorpsePopulate
- NPC damage multiplierScales damage NPCs deal to players and damage players deal to NPCs independently, with per-NPC-type overrides, using Oxide'sโฆOnEntityTakeDamage
Last updated Oct 26, 2026.
Customize npc free zones
Customize