Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/miniSpawns a minicopter in front of you and sets you as its owner.vehiclespawncommands.mini
/scrapheliSpawns a scrap transport helicopter in front of you and sets you as its owner.vehiclespawncommands.scrapheli
/rowboatSpawns 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
Permissions
PermissionGrants
vehiclespawncommands.miniAllows spawning a minicopter with /mini.
vehiclespawncommands.scrapheliAllows spawning a scrap transport helicopter with /scrapheli.
vehiclespawncommands.rowboatAllows spawning a rowboat with /rowboat.

How the code works

  1. Vehicle definitions in the config

    The PluginConfig class holds a Vehicles list. Each entry has a Key (the word used in chat), a Display Name shown in messages, a Prefab path, a Cooldown Seconds value and a Spawn Offset distance. 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. LoadConfig falls back to defaults and rewrites the file if parsing fails.

  2. Spawn position in front of the player

    GetSpawnPosition takes player.eyes.position and adds player.eyes.HeadForward() scaled by the configured offset. The Y coordinate is then replaced with TerrainMeta.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. Otherwise Spawn() runs and the record is written.

  3. Owner and the per-player record

    After spawning, OwnerID is set to player.userID.Get() and SendNetworkUpdate() pushes the change to clients. The entity id, type key, owner id and a Spawned At unix timestamp go into an ActiveRecord stored in a Dictionary<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.

  4. Permissions and the active vehicle limit

    Each vehicle key maps to a permission string built from the config key, for example vehiclespawncommands.mini. Init registers all of them. The command method first checks permission.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 at MaxActivePerType the player gets the limit message instead of a second minicopter.

  5. Cooldowns held in memory

    A Dictionary<string, double> stores the last spawn time per player id and vehicle key using UnityEngine.Time.realtimeSinceStartup. After the permission check the command compares the elapsed time with the entry's Cooldown Seconds and 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.

  6. 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 kill and vehicles destroyed by the game. The /vehicle.spawn.release command exists for vehicles that disappear without the hook firing.

  7. Chat commands and language messages

    The three chat commands are registered with [ChatCommand("mini")] and friends, each a thin wrapper that calls TrySpawn(player, key). TrySpawn does the permission, cooldown and limit checks and sends every reply through Msg, which reads from LoadDefaultMessages. 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

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

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

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.

Prompt: Create an Oxide plugin with /mini, /scrapheli and /rowboat chat commands that spawn the vehicle in front of the player, set OwnerID, allow one active vehicle per type per player with a config cooldown and a permission per vehicle, and release slots via OnEntityKill.
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 spawn commands

Customize