How to make a Rust vehicle spawn command plugin (owner-locked)
Adds /mini, /scrapheli and /rowboat chat commands that spawn a vehicle in front of the player, with a permission per vehicle, a per-type active limit, cooldowns and OwnerID set on the spawned entity. It uses OnEntityKill to free the player's slot when their vehicle is destroyed.
Admins who want fast travel for VIPs or event players usually end up spawning vehicles by hand through the console, which means picking prefabs, setting owners and tracking who already has one. This plugin turns that into three chat commands. Each vehicle has its own permission and its own cooldown in the config, and the config sets a maximum number of active vehicles of each type per player (default 1). The spawn position comes from the player's eye position pushed forward and placed at the terrain height for that XZ coordinate, and the entity gets the player's OwnerID so Rust's own ownership checks, decay and cleanup rules apply. A record of each spawned vehicle is kept in memory and in oxide/data, so /vehicle.spawn.release can clean up after a vehicle was lost without being killed. OnEntityKill removes the record the moment the entity is destroyed, which frees the slot for the next spawn.
The plugin code
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Vehicle Spawn Commands", "RustPluginGenerator", "1.0.0")]
[Description("Spawns owner-locked minicopters, scrap helis and rowboats from chat commands with permissions and cooldowns")]
public class VehicleSpawnCommands : RustPlugin
{
private const string PermPrefix = "vehiclespawncommands.";
private const string DataFileName = "VehicleSpawnCommands";
private PluginConfig config;
private Dictionary<ulong, ActiveRecord> active = new Dictionary<ulong, ActiveRecord>();
private Dictionary<string, double> cooldowns = new Dictionary<string, double>();
private class VehicleDefinition
{
[JsonProperty("Key")] public string Key = "";
[JsonProperty("Display Name")] public string DisplayName = "";
[JsonProperty("Prefab")] public string Prefab = "";
[JsonProperty("Cooldown Seconds")] public double CooldownSeconds = 300;
[JsonProperty("Spawn Offset")] public float SpawnOffset = 4f;
}
private class PluginConfig
{
[JsonProperty("Maximum active vehicles per type per player")]
public int MaxActivePerType = 1;
[JsonProperty("Vehicles")]
public List<VehicleDefinition> Vehicles = new List<VehicleDefinition>
{
new VehicleDefinition
{
Key = "mini",
DisplayName = "Minicopter",
Prefab = "assets/content/vehicles/minicopter/minicopter.entity.prefab"
},
new VehicleDefinition
{
Key = "scrapheli",
DisplayName = "Scrap Transport Helicopter",
Prefab = "assets/content/vehicles/scrap heli carrier/scraptransporthelicopter.prefab"
},
new VehicleDefinition
{
Key = "rowboat",
DisplayName = "Rowboat",
Prefab = "assets/content/vehicles/boats/rowboat/rowboat.prefab"
}
};
}
private class ActiveRecord
{
public ulong EntityId;
public string Key;
public ulong OwnerId;
public int SpawnedAt;
}
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>
{
["NoPermission"] = "You do not have permission to spawn a {0}.",
["UnknownVehicle"] = "Usage: /mini, /scrapheli, /rowboat, or /vehicle.spawn.release <key>.",
["Cooldown"] = "You can spawn a {0} again in {1} seconds.",
["LimitReached"] = "You already have {0} {1} active. Release it with /vehicle.spawn.release {2}.",
["SpawnFailed"] = "The {0} could not be spawned. Try a different spot.",
["Spawned"] = "You spawned a {0}.",
["Released"] = "Your {0} record was released.",
["NothingToRelease"] = "You have no active {0} record.",
["DataCorrupt"] = "Saved vehicle data could not be read and was reset."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
LoadData();
foreach (var vehicle in config.Vehicles)
{
if (!string.IsNullOrEmpty(vehicle.Key))
permission.RegisterPermission(PermPrefix + vehicle.Key, this);
}
}
private void OnServerInitialized() => PruneMissing();
private void Unload() => SaveData();
[ChatCommand("mini")]
private void CmdMini(BasePlayer player, string command, string[] args) => TrySpawn(player, "mini");
[ChatCommand("scrapheli")]
private void CmdScrapHeli(BasePlayer player, string command, string[] args) => TrySpawn(player, "scrapheli");
[ChatCommand("rowboat")]
private void CmdRowboat(BasePlayer player, string command, string[] args) => TrySpawn(player, "rowboat");
[ChatCommand("vehicle.spawn.release")]
private void CmdRelease(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (args == null || args.Length < 1)
{
SendReply(player, Msg("UnknownVehicle", player.UserIDString));
return;
}
var key = args[0].ToLowerInvariant();
var definition = FindDefinition(key);
if (definition == null)
{
SendReply(player, Msg("UnknownVehicle", player.UserIDString));
return;
}
PruneMissing();
var records = active.Values.Where(r => r.OwnerId == player.userID.Get() && r.Key == key).ToList();
if (records.Count == 0)
{
SendReply(player, Msg("NothingToRelease", player.UserIDString, definition.DisplayName));
return;
}
foreach (var record in records)
active.Remove(record.EntityId);
SaveData();
SendReply(player, Msg("Released", player.UserIDString, definition.DisplayName));
}
private void TrySpawn(BasePlayer player, string key)
{
if (player == null || !player.IsConnected) return;
var definition = FindDefinition(key);
if (definition == null)
{
SendReply(player, Msg("UnknownVehicle", player.UserIDString));
return;
}
var perm = PermPrefix + definition.Key;
if (!permission.UserHasPermission(player.UserIDString, perm))
{
SendReply(player, Msg("NoPermission", player.UserIDString, definition.DisplayName));
return;
}
var cooldownKey = player.UserIDString + ":" + definition.Key;
double last;
if (cooldowns.TryGetValue(cooldownKey, out last))
{
var remaining = definition.CooldownSeconds - (UnityEngine.Time.realtimeSinceStartup - last);
if (remaining > 0)
{
SendReply(player, Msg("Cooldown", player.UserIDString, definition.DisplayName, Math.Ceiling(remaining)));
return;
}
}
PruneMissing();
var owned = active.Values.Count(r => r.OwnerId == player.userID.Get() && r.Key == definition.Key);
if (owned >= Math.Max(1, config.MaxActivePerType))
{
SendReply(player, Msg("LimitReached", player.UserIDString, owned, definition.DisplayName, definition.Key));
return;
}
var position = GetSpawnPosition(player, definition);
var entity = GameManager.server.CreateEntity(definition.Prefab, position, Quaternion.identity);
if (entity == null)
{
SendReply(player, Msg("SpawnFailed", player.UserIDString, definition.DisplayName));
return;
}
entity.OwnerID = player.userID.Get();
entity.Spawn();
entity.SendNetworkUpdate();
active[entity.net.ID.Value] = new ActiveRecord
{
EntityId = entity.net.ID.Value,
Key = definition.Key,
OwnerId = player.userID.Get(),
SpawnedAt = Facepunch.Math.Epoch.Current
};
cooldowns[cooldownKey] = UnityEngine.Time.realtimeSinceStartup;
SaveData();
SendReply(player, Msg("Spawned", player.UserIDString, definition.DisplayName));
}
private object OnEntityKill(BaseNetworkable entity)
{
if (entity == null || entity.net == null) return null;
var id = entity.net.ID.Value;
if (!active.ContainsKey(id)) return null;
active.Remove(id);
SaveData();
return null;
}
private Vector3 GetSpawnPosition(BasePlayer player, VehicleDefinition definition)
{
var position = player.eyes.position + player.eyes.HeadForward() * definition.SpawnOffset;
position.y = TerrainMeta.HeightMap.GetHeight(position);
return position;
}
private VehicleDefinition FindDefinition(string key)
{
return config.Vehicles.FirstOrDefault(v => string.Equals(v.Key, key, StringComparison.OrdinalIgnoreCase));
}
private void PruneMissing()
{
if (active.Count == 0) return;
var stale = new List<ulong>();
foreach (var pair in active)
{
var entity = FindEntity(pair.Key);
if (entity == null || entity.IsDestroyed)
stale.Add(pair.Key);
}
if (stale.Count == 0) return;
foreach (var id in stale)
active.Remove(id);
SaveData();
}
private BaseEntity FindEntity(ulong id)
{
foreach (var networkable in BaseNetworkable.serverEntities)
{
var entity = networkable as BaseEntity;
if (entity != null && entity.net != null && entity.net.ID.Value == id)
return entity;
}
return null;
}
private void LoadData()
{
try
{
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, ActiveRecord>>(DataFileName);
active = loaded ?? new Dictionary<ulong, ActiveRecord>();
}
catch
{
PrintWarning(Msg("DataCorrupt"));
active = new Dictionary<ulong, ActiveRecord>();
}
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject(DataFileName, active);
}
}Hooks used: OnServerInitialized, OnEntityKill
Config file
Created automatically on first load at oxide/config/VehicleSpawnCommands.json.
{
"Maximum active vehicles per type per player": 1,
"Vehicles": [
{
"Key": "mini",
"Display Name": "Minicopter",
"Prefab": "assets/content/vehicles/minicopter/minicopter.entity.prefab",
"Cooldown Seconds": 300,
"Spawn Offset": 4
},
{
"Key": "scrapheli",
"Display Name": "Scrap Transport Helicopter",
"Prefab": "assets/content/vehicles/scrap heli carrier/scraptransporthelicopter.prefab",
"Cooldown Seconds": 300,
"Spawn Offset": 4
},
{
"Key": "rowboat",
"Display Name": "Rowboat",
"Prefab": "assets/content/vehicles/boats/rowboat/rowboat.prefab",
"Cooldown Seconds": 300,
"Spawn Offset": 4
}
]
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/mini | Spawns a minicopter in front of you and sets you as its owner. | vehiclespawncommands.mini |
/scrapheli | Spawns a scrap transport helicopter in front of you and sets you as its owner. | vehiclespawncommands.scrapheli |
/rowboat | Spawns a rowboat in front of you and sets you as its owner. | vehiclespawncommands.rowboat |
/vehicle.spawn.release <mini|scrapheli|rowboat> | Removes your active record for that vehicle type without killing the vehicle. | Everyone |
| Permission | Grants |
|---|---|
vehiclespawncommands.mini | Allows spawning a minicopter with /mini. |
vehiclespawncommands.scrapheli | Allows spawning a scrap transport helicopter with /scrapheli. |
vehiclespawncommands.rowboat | Allows spawning a rowboat with /rowboat. |
How the code works
Vehicle definitions in the config
The
PluginConfigclass holds aVehicleslist. Each entry has aKey(the word used in chat), aDisplay Nameshown in messages, aPrefabpath, aCooldown Secondsvalue and aSpawn Offsetdistance. All three defaults are filled in, so a fresh install works without editing anything. Changing a prefab or adding a fourth vehicle only touches the config file.LoadConfigfalls back to defaults and rewrites the file if parsing fails.Spawn position in front of the player
GetSpawnPositiontakesplayer.eyes.positionand addsplayer.eyes.HeadForward()scaled by the configured offset. The Y coordinate is then replaced withTerrainMeta.HeightMap.GetHeight(pos)so the vehicle is placed at the terrain height for that XZ coordinate, which may not match the visual ground if there are overhangs or caves.GameManager.server.CreateEntity(prefab, pos, Quaternion.identity)returns the entity; if it is null the command replies with the failure message and nothing is recorded. OtherwiseSpawn()runs and the record is written.Owner and the per-player record
After spawning,
OwnerIDis set toplayer.userID.Get()andSendNetworkUpdate()pushes the change to clients. The entity id, type key, owner id and aSpawned Atunix timestamp go into anActiveRecordstored in aDictionary<ulong, ActiveRecord>keyed by entity id. Indexes by player id are rebuilt with LINQ when needed, which keeps the code simple; the list only holds what players spawned through the commands, so it stays small.Permissions and the active vehicle limit
Each vehicle key maps to a permission string built from the config key, for example
vehiclespawncommands.mini.Initregisters all of them. The command method first checkspermission.UserHasPermission(player.UserIDString, perm), then the cooldown, then removes stale records whose entity is gone or destroyed, then counts records that belong to the player and match the key. If the count is atMaxActivePerTypethe player gets the limit message instead of a second minicopter.Cooldowns held in memory
A
Dictionary<string, double>stores the last spawn time per player id and vehicle key usingUnityEngine.Time.realtimeSinceStartup. After the permission check the command compares the elapsed time with the entry'sCooldown Secondsand reports the remaining seconds rounded up. Cooldowns reset on plugin reload and on server restart; they are an anti-spam guard, not an economy feature. Pair the plugin with a permission manager when you sell access.OnEntityKill frees the slot
The hook receives every networkable that gets killed, so the first thing it does is look the id up in the dictionary. A miss returns immediately, which is the common case for entities not spawned by this plugin. A hit removes the record and saves data. This covers admins killing vehicles with
ent killand vehicles destroyed by the game. The/vehicle.spawn.releasecommand exists for vehicles that disappear without the hook firing.Chat commands and language messages
The three chat commands are registered with
[ChatCommand("mini")]and friends, each a thin wrapper that callsTrySpawn(player, key).TrySpawndoes the permission, cooldown and limit checks and sends every reply throughMsg, which reads fromLoadDefaultMessages. Keys cover no permission, unknown vehicle, cooldown with seconds, limit reached, spawn failed, success, released and nothing to release. No chat text ever reaches the player without passing through lang.
Install it on your server
Save VehicleSpawnCommands.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/VehicleSpawnCommands.json(Carbon:carbon/configs/VehicleSpawnCommands.json). After editing, reload it:oxide.reload VehicleSpawnCommands c.reload VehicleSpawnCommands # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default vehiclespawncommands.mini c.grant group default vehiclespawncommands.mini # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Kit-item cost
Charge a configured item from the player's inventory before spawning.
Generate this versionTimed despawn
Kill the spawned vehicle after a configured lifetime.
Generate this versionTeam sharing
Let teammates ride under the owner's permission and limit.
Generate this version
FAQ
Does the spawned vehicle belong to the player for Rust's own rules?
Setting OwnerID to the player's id is what the game reads for vehicle ownership, so the player is treated as the owner by Rust systems that check it. The plugin itself only uses its own record to enforce limits and to release slots; it does not modify Rust's ownership logic.
Why did my spawn slot stay occupied after the vehicle vanished?
Slots are released by OnEntityKill. If the entity was removed in a way that did not run that hook, the record stays. Run /vehicle.spawn.release <type>; the command calls PruneMissing before it checks records, so stale records whose entity id no longer resolves are cleared. The next /mini call also calls PruneMissing, so it will clear the stale record as well.
Where do cooldowns live?
In memory only, using UnityEngine.Time.realtimeSinceStartup, so a reload or restart resets them. If you need durable cooldowns across restarts, store the unix time in the data file instead and compare against Facepunch.Math.Epoch.Current. The current behavior means a restart gives players a fresh cooldown window.
Can players spawn in water or on a slope?
The spawn position is the eye position plus the configured forward offset, with Y replaced by the terrain height map value. The vehicle sits at that height, which is the topmost terrain height at that XZ coordinate. On a slope it sits at the terrain height for that point; if there are overhangs or caves the height map value may not match the visual ground, so the vehicle could appear above or below the visible surface. Raise Spawn Offset slightly if vehicles clip into the ground on hills.
How do I install it?
Drop the .cs file into oxide/plugins on an Oxide or Carbon server and let it compile. Grant the permissions you want to sell from your permission manager or the console, then edit oxide/config/VehicleSpawnCommands.json to change prefabs, cooldowns or offsets followed by oxide.reload VehicleSpawnCommands.
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 spawn commands
Customize