Skip to content
RustPluginGenerator

How to make a Rust vehicle limit plugin (per player, per type)

Caps how many vehicles each player can own per vehicle type, blocking purchases and entity spawns once the limit is reached and allowing higher caps through permissions. It uses OnEntitySpawned and OnEntityKill to count and track vehicles.

Large Rust servers leak vehicles. Players buy minicopters and boats, drive them into the sea, and never pick them up. Every one of those vehicles is a networked entity that eats memory and keeps physics awake, and eventually the map is full of abandoned scrap. The usual fix is a manual wipe of unused vehicles, which punishes active players.

This plugin enforces a hard cap per player per vehicle type. It counts owned vehicles at the moment a new one appears (OnEntitySpawned), blocks the spawn if the player is already at their cap, and frees a slot when a vehicle is killed (OnEntityKill). Base limits come from the config, and permission nodes raise the cap for VIP groups. Vehicles owned by NPCs, admins on a configured bypass list, and vehicles already counted on load are handled so existing bases are not wiped on install.

The plugin code

VehicleLimits.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Vehicle Limits", "RustPluginGenerator", "1.0.0")]
    [Description("Caps how many vehicles each player can own per vehicle type, with permission-based extra slots.")]
    public class VehicleLimits : RustPlugin
    {
        private const string PermUse = "vehiclelimits.use";
        private const string PermAdmin = "vehiclelimits.admin";
        private const string PermExtra = "vehiclelimits.extra";

        private PluginConfig config;
        private readonly Dictionary<ulong, EntityRef> tracked = new Dictionary<ulong, EntityRef>();

        private class EntityRef
        {
            public ulong OwnerId;
            public string Key;
        }

        private class VehicleEntry
        {
            [JsonProperty("Readable name")]
            public string DisplayName { get; set; } = "Vehicle";

            [JsonProperty("Prefab short name contains")]
            public string Prefab { get; set; } = "";

            [JsonProperty("Base limit (0 = uncapped)")]
            public int Limit { get; set; } = 1;
        }

        private class PluginConfig
        {
            [JsonProperty("Extra slots per player with the extra permission")]
            public int ExtraPerPermission { get; set; } = 2;

            [JsonProperty("Kill over-limit vehicles on spawn")]
            public bool KillOverLimit { get; set; } = true;

            [JsonProperty("Notify owner when a spawn is blocked")]
            public bool NotifyOwner { get; set; } = true;

            [JsonProperty("Vehicles")]
            public List<VehicleEntry> Vehicles { get; set; } = new List<VehicleEntry>
            {
                new VehicleEntry { DisplayName = "Minicopter", Prefab = "minicopter.entity", Limit = 2 },
                new VehicleEntry { DisplayName = "Scrap Transport Helicopter", Prefab = "scraptransporthelicopter", Limit = 1 },
                new VehicleEntry { DisplayName = "Rowboat", Prefab = "rowboat", Limit = 3 },
                new VehicleEntry { DisplayName = "RHIB", Prefab = "rhib", Limit = 2 }
            };
        }

        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>
            {
                ["Blocked"] = "You already own {0} {1} (limit {2}). Destroy one before spawning another.",
                ["NoPermission"] = "You do not have permission to use this command.",
                ["ListHeader"] = "Your vehicles:",
                ["ListLine"] = "{0}: {1}/{2}",
                ["TargetHeader"] = "Vehicles owned by {0}:",
                ["TargetNotFound"] = "Player '{0}' not found.",
                ["RescanDone"] = "Rescan complete. Tracking {0} vehicles."
            }, 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(PermExtra, this);
        }

        private void OnServerInitialized()
        {
            Rebuild();
        }

        private void Unload()
        {
            tracked.Clear();
        }

        private void Rebuild()
        {
            tracked.Clear();
            foreach (var networkable in BaseNetworkable.serverEntities)
            {
                var entity = networkable as BaseEntity;
                if (entity == null) continue;
                Track(entity, false);
            }
        }

        private VehicleEntry FindEntry(string shortName)
        {
            if (string.IsNullOrEmpty(shortName) || config?.Vehicles == null) return null;
            foreach (var entry in config.Vehicles)
            {
                if (entry == null || string.IsNullOrEmpty(entry.Prefab) || entry.Limit <= 0) continue;
                if (shortName.IndexOf(entry.Prefab, StringComparison.OrdinalIgnoreCase) >= 0)
                    return entry;
            }
            return null;
        }

        private int GetLimit(BasePlayer player, VehicleEntry entry)
        {
            int limit = entry.Limit;
            if (player != null && permission.UserHasPermission(player.UserIDString, PermExtra))
                limit += Math.Max(0, config.ExtraPerPermission);
            return limit;
        }

        private int Count(ulong ownerId, VehicleEntry entry)
        {
            int count = 0;
            foreach (var pair in tracked)
            {
                if (pair.Value.OwnerId == ownerId && pair.Value.Key == entry.Prefab)
                    count++;
            }
            return count;
        }

        private bool Track(BaseEntity entity, bool enforce)
        {
            if (entity == null || entity.IsDestroyed) return false;
            var entry = FindEntry(entity.ShortPrefabName);
            if (entry == null) return false;

            ulong entityId = entity.net.ID.Value;
            if (tracked.ContainsKey(entityId)) return false;

            ulong ownerId = entity.OwnerID;
            if (ownerId == 0) return false;

            BasePlayer owner = BasePlayer.FindByID(ownerId);
            if (owner != null && (owner.IsAdmin || owner.IsNpc)) return false;

            if (enforce)
            {
                int limit = GetLimit(owner, entry);
                int current = Count(ownerId, entry);
                if (current >= limit)
                {
                    if (config.KillOverLimit)
                    {
                        if (owner != null && owner.IsConnected && config.NotifyOwner)
                            owner.ChatMessage(Msg("Blocked", owner.UserIDString, current, entry.DisplayName, limit));
                        entity.Kill();
                    }
                    return false;
                }
            }

            tracked[entityId] = new EntityRef { OwnerId = ownerId, Key = entry.Prefab };
            return true;
        }

        private void OnEntitySpawned(BaseNetworkable entity)
        {
            Track(entity as BaseEntity, true);
        }

        private object OnEntityKill(BaseNetworkable entity)
        {
            if (entity != null && entity.net != null)
                tracked.Remove(entity.net.ID.Value);
            return null;
        }

        [ChatCommand("vlimit")]
        private void CmdVLimit(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 > 0)
            {
                if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
                {
                    player.ChatMessage(Msg("NoPermission", player.UserIDString));
                    return;
                }
                string target = string.Join(" ", args);
                BasePlayer other = BasePlayer.FindAwakeOrSleeping(target);
                if (other == null)
                {
                    player.ChatMessage(Msg("TargetNotFound", player.UserIDString, target));
                    return;
                }
                player.ChatMessage(Msg("TargetHeader", player.UserIDString, other.displayName));
                PrintList(player, other);
                return;
            }

            player.ChatMessage(Msg("ListHeader", player.UserIDString));
            PrintList(player, player);
        }

        private void PrintList(BasePlayer to, BasePlayer target)
        {
            foreach (var entry in config.Vehicles)
            {
                if (entry == null || entry.Limit <= 0) continue;
                int count = Count(target.userID.Get(), entry);
                int limit = GetLimit(target, entry);
                to.ChatMessage(Msg("ListLine", to.UserIDString, entry.DisplayName, count, limit));
            }
        }

        [ConsoleCommand("vehiclelimits.rescan")]
        private void CcmdRescan(ConsoleSystem.Arg arg)
        {
            if (arg == null) return;
            var player = arg.Player();
            if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                arg.ReplyWith(Msg("NoPermission", player.UserIDString));
                return;
            }
            Rebuild();
            arg.ReplyWith(Msg("RescanDone", player?.UserIDString, tracked.Count));
        }
    }
}

Hooks used: OnServerInitialized, OnEntitySpawned, OnEntityKill

Config file

Created automatically on first load at oxide/config/VehicleLimits.json.

VehicleLimits.json
{
  "Extra slots per player with the extra permission": 2,
  "Kill over-limit vehicles on spawn": true,
  "Notify owner when a spawn is blocked": true,
  "Vehicles": [
    {
      "Readable name": "Minicopter",
      "Prefab short name contains": "minicopter.entity",
      "Base limit (0 = uncapped)": 2
    },
    {
      "Readable name": "Scrap Transport Helicopter",
      "Prefab short name contains": "scraptransporthelicopter",
      "Base limit (0 = uncapped)": 1
    },
    {
      "Readable name": "Rowboat",
      "Prefab short name contains": "rowboat",
      "Base limit (0 = uncapped)": 3
    },
    {
      "Readable name": "RHIB",
      "Prefab short name contains": "rhib",
      "Base limit (0 = uncapped)": 2
    }
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/vlimit [name or steamid]Shows your current vehicle counts and caps; with the admin permission, shows another player's counts.vehiclelimits.use
vehiclelimits.rescanRebuilds the tracked vehicle counts from all spawned entities.vehiclelimits.admin
Permissions
PermissionGrants
vehiclelimits.useAllows the /vlimit command.
vehiclelimits.adminAllows /vlimit on other players and vehiclelimits.rescan.
vehiclelimits.extraRaises the cap of every vehicle type by the configured ExtraPerPermission value.

How the code works

  1. Config: vehicle types, base caps and permission bonus

    The PluginConfig class holds an ExtraPerPermission value, two booleans, and a Vehicles list of entries. Each entry has a readable name, the prefab short name the entity reports through ShortPrefabName, and the base Limit. The prefab names are matched as substrings, so minicopter.entity also matches variant names you extend later. Add one entry per type you want to cap and set Limit to 0 to disable the cap for that type.

  2. Counting on spawn with OnEntitySpawned

    OnEntitySpawned(BaseNetworkable entity) casts the argument to BaseEntity, reads ShortPrefabName and looks it up in the config. If the short name matches a capped type, the plugin reads OwnerID and asks BasePlayer.FindByID for the owner. Server-owned vehicles (OwnerID 0), NPC-owned entities and admin-owned entities are ignored. The owner's current count is checked against GetLimit, which starts at the configured base and adds ExtraPerPermission when the player holds the extra permission.

  3. Blocking the over-limit spawn

    When the count reaches the cap, the plugin kills the entity with Kill() and, if the owner is connected and NotifyOwner is on, sends the translated message plus the current cap. Blocking at the entity level covers every path: bought vehicles, crafted items dropped from inventory, admin spawns and plugin spawns. The owner keeps the item if the entity never existed, so you may want to also block the purchase in your shop.

  4. Freeing slots with OnEntityKill

    OnEntityKill(BaseNetworkable entity) runs whenever an entity is destroyed. The plugin looks the entity up in the tracked dictionary that stores an owner id and vehicle key for every counted vehicle. If it finds a match it removes the entry. The dictionary is the source of truth for counts, so a vehicle that despawns, is destroyed by damage, or is removed by a wipe stops counting immediately. Every destroyed vehicle releases exactly one entry.

  5. Reconciling on load and rescan

    In OnServerInitialized the plugin iterates BaseNetworkable.serverEntities, tracks each match, and rebuilds the dictionary. Existing servers therefore start with accurate counts after every restart instead of an empty map. The vehiclelimits.rescan console command runs the same pass manually and replies with the number of tracked vehicles, which helps after a plugin reload during a live session. The initial pass never kills anything, only counts.

  6. Showing caps with /vlimit

    The [ChatCommand("vlimit")] method walks the configured types, calls Count for the player and GetLimit for the effective cap, and prints a line per type through the lang messages. With an argument and the admin permission it resolves the target through BasePlayer.FindAwakeOrSleeping. The command does not touch the entity list, so it is cheap to run frequently.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip:oxide.grant group default vehiclelimits.use c.grant group default vehiclelimits.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Global server cap instead of per player

    Limits the total number of each vehicle type on the whole map instead of per player.

    Generate this version
  • Decay timer for empty vehicles

    Removes vehicles that stay unused for a configurable time to free capacity.

    Generate this version
  • Team-shared limits

    Counts vehicles across all members of a team instead of a single player.

    Generate this version

FAQ

Does this plugin stop players from buying a vehicle they cannot own?

It stops the vehicle entity from existing. Depending on the purchase path, the item may be consumed before the entity spawns, so pair it with a check in your shop plugin if refunds matter. The plugin itself only blocks the spawn and frees nothing else.

How do I give VIPs more vehicles?

Grant the vehiclelimits.extra permission and set ExtraPerPermission in the config. Oxide permissions stack, so multiple grants of the same node do not add up; raise the number or use a different node per tier with a custom GetLimit.

Why do already placed vehicles count after install?

OnServerInitialized re-scans BaseNetworkable.serverEntities and counts every matching entity it finds, so existing vehicles are included immediately. Use the rescan console command after reloading to rebuild the counts during a session.

Does it work on Carbon?

Yes. Carbon loads the same plugin file and calls the same hooks. Install it under oxide/plugins or Carbon's plugin folder for your build and it behaves identically.

Which vehicles are covered by default?

The default config caps minicopters, scrap transport helicopters, rowboats and rhibs. Any vehicle you can name through its prefab short name can be added as a new entry, including modular cars and horses, as long as the entity reports that name.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Create a Rust plugin that caps per-player vehicle ownership by prefab type, blocking over-limit spawns with a chat message, freeing slots on kill, and raising the cap via permission.
Customize for my server

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

Last updated Oct 26, 2026.

Customize vehicle limits per player

Customize