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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/vlimit [name or steamid] | Shows your current vehicle counts and caps; with the admin permission, shows another player's counts. | vehiclelimits.use |
vehiclelimits.rescan | Rebuilds the tracked vehicle counts from all spawned entities. | vehiclelimits.admin |
| Permission | Grants |
|---|---|
vehiclelimits.use | Allows the /vlimit command. |
vehiclelimits.admin | Allows /vlimit on other players and vehiclelimits.rescan. |
vehiclelimits.extra | Raises the cap of every vehicle type by the configured ExtraPerPermission value. |
How the code works
Config: vehicle types, base caps and permission bonus
The
PluginConfigclass holds anExtraPerPermissionvalue, two booleans, and aVehicleslist of entries. Each entry has a readable name, the prefab short name the entity reports throughShortPrefabName, and the baseLimit. The prefab names are matched as substrings, sominicopter.entityalso matches variant names you extend later. Add one entry per type you want to cap and setLimitto 0 to disable the cap for that type.Counting on spawn with OnEntitySpawned
OnEntitySpawned(BaseNetworkable entity)casts the argument toBaseEntity, readsShortPrefabNameand looks it up in the config. If the short name matches a capped type, the plugin readsOwnerIDand asksBasePlayer.FindByIDfor the owner. Server-owned vehicles (OwnerID 0), NPC-owned entities and admin-owned entities are ignored. The owner's current count is checked againstGetLimit, which starts at the configured base and addsExtraPerPermissionwhen the player holds the extra permission.Blocking the over-limit spawn
When the count reaches the cap, the plugin kills the entity with
Kill()and, if the owner is connected andNotifyOwneris 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.Freeing slots with OnEntityKill
OnEntityKill(BaseNetworkable entity)runs whenever an entity is destroyed. The plugin looks the entity up in thetrackeddictionary 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.Reconciling on load and rescan
In
OnServerInitializedthe plugin iteratesBaseNetworkable.serverEntities, tracks each match, and rebuilds the dictionary. Existing servers therefore start with accurate counts after every restart instead of an empty map. Thevehiclelimits.rescanconsole 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.Showing caps with /vlimit
The
[ChatCommand("vlimit")]method walks the configured types, callsCountfor the player andGetLimitfor the effective cap, and prints a line per type through the lang messages. With an argument and the admin permission it resolves the target throughBasePlayer.FindAwakeOrSleeping. The command does not touch the entity list, so it is cheap to run frequently.
Install it on your server
Save VehicleLimits.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/VehicleLimits.json(Carbon:carbon/configs/VehicleLimits.json). After editing, reload it:oxide.reload VehicleLimits c.reload VehicleLimits # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionDecay timer for empty vehicles
Removes vehicles that stay unused for a configurable time to free capacity.
Generate this versionTeam-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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More vehicles plugins
- Car spawn commandAdds a /car command that spawns a modular car with configured modules in front of the player, enforcing a permission, a spawn cooldown and a strictโฆOnEntityKill
- Horse spawn commandAdds a /horse command that spawns a ridable horse beside the player, tracks one horse per player, removes that player's previous horse and enforcesโฆOnServerInitializedOnEntityKill
- No vehicle decayScales or disables decay damage on minicopters, boats, horses and modular cars per vehicle type, optionally only while inside a tool cupboard radius.OnEntityTakeDamage
Last updated Oct 26, 2026.
Customize vehicle limits per player
Customize