How to make a Rust deployable limits plugin (per-prefab caps with VIP tiers)
Caps how many of each deployable (by short prefab name) a player can own, with per-permission-group limits, using OnEntityBuilt to enforce, CanBuild to block placement early, and OnEntityKill plus a startup scan of BaseNetworkable.serverEntities to keep counts accurate.
Large servers drown in turrets and boxes, and a flat global cap punishes builders while doing nothing for VIPs. This plugin gives every player a limit per prefab, and players holding a VIP permission get a different number. Counting is kept in memory: every build is added on OnEntityBuilt, every kill removes one on OnEntityKill, and the whole table is rebuilt from BaseNetworkable.serverEntities on startup so a restart never drifts. Enforcement runs in OnEntityBuilt: when a player is at the cap the fresh entity is killed and the player gets a chat line with the current count, the cap and the display name. CanBuild adds an optional early gate that refuses construction when the player is already at any capped limit. Limits live in a config file keyed by short prefab name, each with a default limit and an optional list of permission tiers.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Deployable Limits", "RustPluginGenerator", "1.0.0")]
[Description("Caps how many of each deployable (by short prefab name) a player can own, with permission-based VIP tiers.")]
public class DeployableLimits : RustPlugin
{
private const string PermUse = "deployablelimits.use";
private const string PermAdmin = "deployablelimits.admin";
private PluginConfig config;
private readonly Dictionary<string, LimitRule> rulesByPrefab = new Dictionary<string, LimitRule>();
private readonly Dictionary<ulong, Dictionary<string, int>> counts = new Dictionary<ulong, Dictionary<string, int>>();
private class LimitRule
{
[JsonProperty("Prefab short name")]
public string Prefab { get; set; } = "";
[JsonProperty("Display name")]
public string DisplayName { get; set; } = "";
[JsonProperty("Default limit (0 = unlimited)")]
public int DefaultLimit { get; set; } = 0;
[JsonProperty("Permission tiers")]
public List<LimitTier> Tiers { get; set; } = new List<LimitTier>();
}
private class LimitTier
{
[JsonProperty("Permission")]
public string Permission { get; set; } = "";
[JsonProperty("Limit")]
public int Limit { get; set; } = 0;
}
private class PluginConfig
{
[JsonProperty("Block building when over any capped limit")]
public bool BlockBuildWhenOverLimit { get; set; } = false;
[JsonProperty("Rules")]
public List<LimitRule> Rules { get; set; } = new List<LimitRule>
{
new LimitRule
{
Prefab = "autoturret_deployed",
DisplayName = "Auto Turret",
DefaultLimit = 6,
Tiers = new List<LimitTier>
{
new LimitTier { Permission = "deployablelimits.vip", Limit = 12 }
}
},
new LimitRule
{
Prefab = "box.wooden.large",
DisplayName = "Large Wooden Box",
DefaultLimit = 20,
Tiers = new List<LimitTier>
{
new LimitTier { Permission = "deployablelimits.vip", Limit = 40 }
}
},
new LimitRule
{
Prefab = "cupboard.tool.deployed",
DisplayName = "Tool Cupboard",
DefaultLimit = 1,
Tiers = new List<LimitTier>()
},
new LimitRule
{
Prefab = "sleepingbag_leather_deployed",
DisplayName = "Sleeping Bag",
DefaultLimit = 8,
Tiers = new List<LimitTier>()
}
};
}
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>
{
["LimitReached"] = "You already have {0}/{1} of {2}.",
["LimitWarning"] = "You are over the limit for {0} ({1}/{2}). Nothing was destroyed.",
["NoRules"] = "No deployable limits are configured.",
["NoPermission"] = "You do not have permission to use this command.",
["Usage"] = "{0}: {1}/{2}",
["Reloaded"] = "Deployable limits reloaded and recounted.",
["UnknownPrefab"] = "No limit is configured for '{0}'."
}, 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);
foreach (var rule in config.Rules)
{
if (rule == null || rule.Tiers == null) continue;
foreach (var tier in rule.Tiers)
{
if (tier == null || string.IsNullOrEmpty(tier.Permission)) continue;
if (!permission.PermissionExists(tier.Permission, this))
permission.RegisterPermission(tier.Permission, this);
}
}
}
private void OnServerInitialized()
{
RebuildRules();
RecountEntities();
}
private void RebuildRules()
{
rulesByPrefab.Clear();
foreach (var rule in config.Rules)
{
if (rule == null || string.IsNullOrEmpty(rule.Prefab)) continue;
rulesByPrefab[rule.Prefab] = rule;
}
}
private void RecountEntities()
{
counts.Clear();
foreach (var net in BaseNetworkable.serverEntities)
{
var entity = net as BaseEntity;
if (entity == null) continue;
var owner = entity.OwnerID;
if (owner == 0UL) continue;
var shortName = entity.ShortPrefabName;
if (string.IsNullOrEmpty(shortName) || !rulesByPrefab.ContainsKey(shortName)) continue;
Increment(owner, shortName);
}
}
private void Increment(ulong owner, string prefab)
{
Dictionary<string, int> perPlayer;
if (!counts.TryGetValue(owner, out perPlayer))
{
perPlayer = new Dictionary<string, int>();
counts[owner] = perPlayer;
}
int current;
perPlayer.TryGetValue(prefab, out current);
perPlayer[prefab] = current + 1;
}
private void Decrement(ulong owner, string prefab)
{
Dictionary<string, int> perPlayer;
if (!counts.TryGetValue(owner, out perPlayer)) return;
int current;
if (!perPlayer.TryGetValue(prefab, out current)) return;
if (current <= 1) perPlayer.Remove(prefab);
else perPlayer[prefab] = current - 1;
if (perPlayer.Count == 0) counts.Remove(owner);
}
private int CountOf(ulong owner, string prefab)
{
Dictionary<string, int> perPlayer;
if (!counts.TryGetValue(owner, out perPlayer)) return 0;
int current;
return perPlayer.TryGetValue(prefab, out current) ? current : 0;
}
private int LimitFor(LimitRule rule, BasePlayer player)
{
if (rule.Tiers != null)
{
foreach (var tier in rule.Tiers)
{
if (tier == null || string.IsNullOrEmpty(tier.Permission)) continue;
if (permission.UserHasPermission(player.UserIDString, tier.Permission)) return tier.Limit;
}
}
return rule.DefaultLimit;
}
private object OnEntityBuilt(Planner planner, GameObject gameObject)
{
if (planner == null || gameObject == null) return null;
var entity = gameObject.ToBaseEntity();
if (entity == null) return null;
var shortName = entity.ShortPrefabName;
if (string.IsNullOrEmpty(shortName)) return null;
LimitRule rule;
if (!rulesByPrefab.TryGetValue(shortName, out rule)) return null;
if (rule.DefaultLimit <= 0 && (rule.Tiers == null || rule.Tiers.Count == 0)) return null;
var player = planner.GetOwnerPlayer();
if (player == null) return null;
var limit = LimitFor(rule, player);
var name = string.IsNullOrEmpty(rule.DisplayName) ? shortName : rule.DisplayName;
var current = CountOf(player.userID.Get(), shortName);
if (limit > 0 && current >= limit)
{
player.ChatMessage(Msg("LimitReached", player.UserIDString, current, limit, name));
entity.Kill();
return null;
}
Increment(player.userID.Get(), shortName);
if (limit > 0 && current + 1 > limit)
player.ChatMessage(Msg("LimitWarning", player.UserIDString, name, current + 1, limit));
return null;
}
private object CanBuild(Planner planner, Construction construction, Construction.Target target)
{
if (!config.BlockBuildWhenOverLimit) return null;
if (planner == null) return null;
var player = planner.GetOwnerPlayer();
if (player == null) return null;
if (player.IsAdmin) return null;
var userId = player.userID.Get();
foreach (var kvp in rulesByPrefab)
{
var limit = LimitFor(kvp.Value, player);
if (limit <= 0) continue;
if (CountOf(userId, kvp.Key) >= limit) return false;
}
return null;
}
private object OnEntityKill(BaseNetworkable entity)
{
if (entity == null) return null;
var ent = entity as BaseEntity;
if (ent == null) return null;
var owner = ent.OwnerID;
if (owner == 0UL) return null;
var shortName = ent.ShortPrefabName;
if (string.IsNullOrEmpty(shortName)) return null;
if (!rulesByPrefab.ContainsKey(shortName)) return null;
Decrement(owner, shortName);
return null;
}
[ChatCommand("dlimits")]
private void CmdLimits(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (args != null && args.Length >= 2 && args[0].Equals("check", StringComparison.OrdinalIgnoreCase))
{
var prefab = args[1];
LimitRule rule;
if (!rulesByPrefab.TryGetValue(prefab, out rule))
{
player.ChatMessage(Msg("UnknownPrefab", player.UserIDString, prefab));
return;
}
var limit = LimitFor(rule, player);
var name = string.IsNullOrEmpty(rule.DisplayName) ? prefab : rule.DisplayName;
player.ChatMessage(Msg("Usage", player.UserIDString, name, CountOf(player.userID.Get(), prefab), limit <= 0 ? "unlimited" : limit.ToString()));
return;
}
ShowAll(player);
}
private void ShowAll(BasePlayer player)
{
if (rulesByPrefab.Count == 0)
{
player.ChatMessage(Msg("NoRules", player.UserIDString));
return;
}
var userId = player.userID.Get();
foreach (var kvp in rulesByPrefab)
{
var limit = LimitFor(kvp.Value, player);
var name = string.IsNullOrEmpty(kvp.Value.DisplayName) ? kvp.Key : kvp.Value.DisplayName;
player.ChatMessage(Msg("Usage", player.UserIDString, name, CountOf(userId, kvp.Key), limit <= 0 ? "unlimited" : limit.ToString()));
}
}
[ConsoleCommand("dlimits.reload")]
private void CcmdReload(ConsoleSystem.Arg arg)
{
if (arg == null) return;
var player = arg.Player();
if (!arg.IsAdmin && (player == null || !permission.UserHasPermission(player.UserIDString, PermAdmin)))
{
arg.ReplyWith("No permission.");
return;
}
LoadConfig();
RebuildRules();
RecountEntities();
arg.ReplyWith(Msg("Reloaded"));
}
private void Unload()
{
counts.Clear();
rulesByPrefab.Clear();
}
}
}Hooks used: OnServerInitialized, OnEntityBuilt, CanBuild, OnEntityKill
Config file
Created automatically on first load at oxide/config/DeployableLimits.json.
{
"Block building when over any capped limit": false,
"Rules": [
{
"Prefab short name": "autoturret_deployed",
"Display name": "Auto Turret",
"Default limit (0 = unlimited)": 6,
"Permission tiers": [
{
"Permission": "deployablelimits.vip",
"Limit": 12
}
]
},
{
"Prefab short name": "box.wooden.large",
"Display name": "Large Wooden Box",
"Default limit (0 = unlimited)": 20,
"Permission tiers": [
{
"Permission": "deployablelimits.vip",
"Limit": 40
}
]
},
{
"Prefab short name": "cupboard.tool.deployed",
"Display name": "Tool Cupboard",
"Default limit (0 = unlimited)": 1,
"Permission tiers": []
},
{
"Prefab short name": "sleepingbag_leather_deployed",
"Display name": "Sleeping Bag",
"Default limit (0 = unlimited)": 8,
"Permission tiers": []
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/dlimits | Lists your current usage and effective limit for every configured prefab. | deployablelimits.use |
/dlimits check <shortprefab> | Shows how many of that prefab you own and what your limit is. | deployablelimits.use |
dlimits.reload | Reloads the config file from disk and rebuilds counts from live entities. | deployablelimits.admin |
| Permission | Grants |
|---|---|
deployablelimits.use | Allows the /dlimits chat command. |
deployablelimits.admin | Allows the dlimits.reload console command. |
deployablelimits.vip | Example VIP tier; grants higher limits configured under each prefab's Tiers list. |
How the code works
Config: one entry per short prefab name
PluginConfigholds aRuleslist ofLimitRuleobjects. Each rule hasPrefab(theShortPrefabName, e.g.autoturret_deployedorbox.wooden.large),DisplayName,DefaultLimit, and aTierslist of{ Permission, Limit }.RebuildRulescopies the rules into aDictionary<string, LimitRule>keyed by prefab so lookups in hot hooks never scan the list. A rule withDefaultLimitof 0 and no tiers is ignored entirely. Tier permissions are registered inInitsoUserHasPermissionworks even before the config is edited.Counting from server entities on startup
OnServerInitializedcallsRebuildRulesthenRecountEntities, which clearscountsand iteratesBaseNetworkable.serverEntities. Each entry is cast withas BaseEntity; anything withOwnerID == 0, an emptyShortPrefabName, or a prefab with no rule is skipped. Surviving entities go intoDictionary<ulong, Dictionary<string, int>>keyed by owner then prefab viaIncrement. Because it is a full rebuild, a plugin reload never over-counts and a manual entity wipe never leaves stale entries.Enforcing on OnEntityBuilt
When the planner places something,
OnEntityBuilt(Planner planner, GameObject gameObject)fires. The plugin resolves the entity withgameObject.ToBaseEntity(), readsShortPrefabName, and looks it up inrulesByPrefab. It takesplanner.GetOwnerPlayer(), computes the player's limit withLimitForand the current count withCountOf. If the count is already at the limit the entity is killed andLimitReachedis sent. OtherwiseIncrementrecords the new entity and, if the new total exceeds the limit, aLimitWarningis sent without any deletion.Early block with CanBuild
CanBuild(Planner planner, Construction construction, Construction.Target target)runs before construction starts. TheConstructionargument describes a socket, not a deployable, so the hook cannot see which prefab is about to be built. The plugin therefore only uses it as a coarse gate: whenBlockBuildWhenOverLimitis true in config and any configured rule is at its cap for the player, it returnsfalse. Admins are exempt. Leave the option off in the default config, becauseOnEntityBuiltalready kills the spawned entity.Decrementing on OnEntityKill
OnEntityKill(BaseNetworkable entity)casts toBaseEntity, readsOwnerIDandShortPrefabName, and callsDecrementwhen the prefab has a rule.Decrementremoves the per-prefab entry when it reaches zero and drops the owner's inner dictionary when it becomes empty, so nothing lingers for players who own nothing. The hook returnsnullso default kill handling is untouched.Unloadclears bothcountsandrulesByPrefaband leaves no data file behind; the next startup scan rebuilds everything.Messages and the commands
LoadDefaultMessagesregistersLimitReached,LimitWarning,NoRules,Usage,ReloadedandUnknownPrefab.Msgwrapslang.GetMessagewith the player'sUserIDString./dlimitseither prints every rule viaUsageor, withcheck <prefab>, prints one.dlimits.reloadchecksarg.IsAdminor the admin permission, then callsLoadConfig,RebuildRulesandRecountEntitiesbefore replying. Limits of 0 show as the literal stringunlimitedin/dlimitsoutput.
Install it on your server
Save DeployableLimits.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/DeployableLimits.json(Carbon:carbon/configs/DeployableLimits.json). After editing, reload it:oxide.reload DeployableLimits c.reload DeployableLimits # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default deployablelimits.use c.grant group default deployablelimits.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Team-scoped limits
Count deployables per team instead of per player so a group of four cannot pool 24 turrets.
Generate this versionPeriodic rescan
Add a timer that re-scans serverEntities so counts self-heal without a plugin reload.
Generate this versionLog over-limit attempts
Record every blocked placement in oxide/logs and message admins online instead of silently killing the entity.
Generate this version
FAQ
Why does OnEntityBuilt sometimes fire for the wrong prefab?
It fires for whatever the planner just built. If you add foundations or tool cupboards to the config, their ShortPrefabName differs (cupboard.tool.deployed, foundation). Check the exact name in game before adding a rule, because a typo means that prefab is never counted and never limited.
Do counts survive a plugin reload?
They are rebuilt on OnServerInitialized by scanning BaseNetworkable.serverEntities and calling RecountEntities. Nothing is written to oxide/data, so there is no drift between restarts. If you edit the config at runtime, run dlimits.reload in the server console to reload, rebuild rules and recount.
What happens if a player builds while over the limit?
OnEntityBuilt runs Increment and, when CountOf returns more than the limit, sends the LimitWarning message. The entity is kept. This only happens after an admin lowers a limit at runtime, because under normal play the kill path fires first when current >= limit.
How does the VIP tier pick a limit?
Each LimitRule.Tiers entry stores a permission and a limit. LimitFor walks the list in order and returns the first entry whose permission the player holds, otherwise DefaultLimit. Tier permissions are registered in Init from the config, and the static deployablelimits.vip example is registered the same way.
Where do I install the plugin file?
Save the compiled .cs file into oxide/plugins/DeployableLimits.cs on the server. Oxide or Carbon compiles it on the next load or reload and writes the default config to oxide/config/DeployableLimits.json. Edit the config, then run dlimits.reload from the server console to apply it without restarting.
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 building plugins
- Auto closing doorsCloses doors a configurable number of seconds after a player opens them, with a per-player /autodoor toggle and a personal delay clamped to serverโฆOnDoorOpened
- Auto code lockAutomatically attaches a code lock with the player's saved personal code to every door or box they place, consuming a code lock from their inventory.OnServerInitializedOnEntityBuilt
- Auto upgrade on buildLets each player pick a building grade with /up 1-4, then upgrades every building block they place to that grade using resources taken from theirโฆOnServerInitializedOnEntityBuilt
Last updated Oct 26, 2026.
Customize deployable limits
Customize