Skip to content
RustPluginGenerator

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

DeployableLimits.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/dlimitsLists 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.reloadReloads the config file from disk and rebuilds counts from live entities.deployablelimits.admin
Permissions
PermissionGrants
deployablelimits.useAllows the /dlimits chat command.
deployablelimits.adminAllows the dlimits.reload console command.
deployablelimits.vipExample VIP tier; grants higher limits configured under each prefab's Tiers list.

How the code works

  1. Config: one entry per short prefab name

    PluginConfig holds a Rules list of LimitRule objects. Each rule has Prefab (the ShortPrefabName, e.g. autoturret_deployed or box.wooden.large), DisplayName, DefaultLimit, and a Tiers list of { Permission, Limit }. RebuildRules copies the rules into a Dictionary<string, LimitRule> keyed by prefab so lookups in hot hooks never scan the list. A rule with DefaultLimit of 0 and no tiers is ignored entirely. Tier permissions are registered in Init so UserHasPermission works even before the config is edited.

  2. Counting from server entities on startup

    OnServerInitialized calls RebuildRules then RecountEntities, which clears counts and iterates BaseNetworkable.serverEntities. Each entry is cast with as BaseEntity; anything with OwnerID == 0, an empty ShortPrefabName, or a prefab with no rule is skipped. Surviving entities go into Dictionary<ulong, Dictionary<string, int>> keyed by owner then prefab via Increment. Because it is a full rebuild, a plugin reload never over-counts and a manual entity wipe never leaves stale entries.

  3. Enforcing on OnEntityBuilt

    When the planner places something, OnEntityBuilt(Planner planner, GameObject gameObject) fires. The plugin resolves the entity with gameObject.ToBaseEntity(), reads ShortPrefabName, and looks it up in rulesByPrefab. It takes planner.GetOwnerPlayer(), computes the player's limit with LimitFor and the current count with CountOf. If the count is already at the limit the entity is killed and LimitReached is sent. Otherwise Increment records the new entity and, if the new total exceeds the limit, a LimitWarning is sent without any deletion.

  4. Early block with CanBuild

    CanBuild(Planner planner, Construction construction, Construction.Target target) runs before construction starts. The Construction argument 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: when BlockBuildWhenOverLimit is true in config and any configured rule is at its cap for the player, it returns false. Admins are exempt. Leave the option off in the default config, because OnEntityBuilt already kills the spawned entity.

  5. Decrementing on OnEntityKill

    OnEntityKill(BaseNetworkable entity) casts to BaseEntity, reads OwnerID and ShortPrefabName, and calls Decrement when the prefab has a rule. Decrement removes 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 returns null so default kill handling is untouched. Unload clears both counts and rulesByPrefab and leaves no data file behind; the next startup scan rebuilds everything.

  6. Messages and the commands

    LoadDefaultMessages registers LimitReached, LimitWarning, NoRules, Usage, Reloaded and UnknownPrefab. Msg wraps lang.GetMessage with the player's UserIDString. /dlimits either prints every rule via Usage or, with check <prefab>, prints one. dlimits.reload checks arg.IsAdmin or the admin permission, then calls LoadConfig, RebuildRules and RecountEntities before replying. Limits of 0 show as the literal string unlimited in /dlimits output.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Periodic rescan

    Add a timer that re-scans serverEntities so counts self-heal without a plugin reload.

    Generate this version
  • Log 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.

Prompt: Rust Oxide plugin DeployableLimits that caps each deployable by short prefab name with per-player limits, VIP permission tiers, OnEntityBuilt enforcement, OnEntityKill decrement, and counts rebuilt from serverEntities on startup.
Customize for my server

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

Last updated Oct 26, 2026.

Customize deployable limits

Customize