Skip to content
RustPluginGenerator

How to make a Rust vehicle recall plugin (despawn command)

Adds a /despawn chat command that removes the vehicle the player is looking at when they own it and nobody is mounted, then refunds a configurable item. It uses no Oxide hooks, only a ChatCommand handler.

Vehicles that are stuck, flipped, out of fuel or parked somewhere the owner cannot reach are a recurring support ticket. Letting players remove their own vehicle keeps the map clean and gives admins a predictable rule instead of manual entity kills. The tricky parts are proving ownership, refusing to delete something someone is sitting in, and making the refund safe when an item shortname is misconfigured.

This plugin adds /despawn. It raycasts from the player's eyes for a short distance, accepts only entities that are BaseVehicle, checks OwnerID against the caller, and refuses when a child of the vehicle reports a mounted entity. On success it spawns the refund item into the player's inventory (dropping it at their feet if the inventory is full) and kills the entity. Range, refund item, amount, cooldown and confirmation are all configurable.

The plugin code

VehicleDespawn.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Vehicle Despawn", "RustPluginGenerator", "1.0.0")]
    [Description("Lets players despawn a vehicle they own and are looking at, refunding a configurable item.")]
    public class VehicleDespawn : RustPlugin
    {
        private const string PermUse = "vehicledespawn.use";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
        private readonly Dictionary<ulong, float> pendingConfirm = new Dictionary<ulong, float>();

        private class PluginConfig
        {
            [JsonProperty("Range (metres)")]
            public float Range { get; set; } = 5f;

            [JsonProperty("Refund item shortname")]
            public string RefundShortname { get; set; } = "scrap";

            [JsonProperty("Refund amount")]
            public int RefundAmount { get; set; } = 100;

            [JsonProperty("Cooldown (seconds)")]
            public float CooldownSeconds { get; set; } = 300f;

            [JsonProperty("Require confirmation")]
            public bool RequireConfirmation { get; set; } = true;

            [JsonProperty("Confirmation window (seconds)")]
            public float ConfirmSeconds { get; set; } = 15f;
        }

        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 don't have permission to use that command.",
                ["NoVehicle"] = "Look at one of your vehicles within {0} metres.",
                ["NotOwner"] = "You can only despawn a vehicle you own.",
                ["Occupied"] = "That vehicle has a player mounted. Unmount first.",
                ["Cooldown"] = "You can despawn another vehicle in {0} seconds.",
                ["Confirm"] = "Type /despawn again within {0} seconds to confirm removal of your {1}.",
                ["Refunded"] = "Vehicle removed. You received {0} x {1}.",
                ["RefundFailed"] = "Vehicle removed, but the refund item could not be created. Check the config."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
        }

        private void OnServerInitialized()
        {
            if (ItemManager.FindItemDefinition(config.RefundShortname) == null)
            {
                PrintWarning($"Refund shortname '{config.RefundShortname}' is not a valid item; despawns will not refund.");
            }
        }

        [ChatCommand("despawn")]
        private void CmdDespawn(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;

            if (!player.IsAdmin && !permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            float now = UnityEngine.Time.realtimeSinceStartup;

            float nextAllowed;
            if (cooldowns.TryGetValue(player.userID.Get(), out nextAllowed) && now < nextAllowed)
            {
                player.ChatMessage(Msg("Cooldown", player.UserIDString, Mathf.CeilToInt(nextAllowed - now)));
                return;
            }

            var vehicle = FindVehicle(player);
            if (vehicle == null)
            {
                player.ChatMessage(Msg("NoVehicle", player.UserIDString, config.Range));
                return;
            }

            if (!player.IsAdmin && vehicle.OwnerID != player.userID.Get())
            {
                player.ChatMessage(Msg("NotOwner", player.UserIDString));
                return;
            }

            if (IsOccupied(vehicle))
            {
                player.ChatMessage(Msg("Occupied", player.UserIDString));
                return;
            }

            if (config.RequireConfirmation)
            {
                float window;
                if (!pendingConfirm.TryGetValue(player.userID.Get(), out window) || now > window)
                {
                    pendingConfirm[player.userID.Get()] = now + config.ConfirmSeconds;
                    player.ChatMessage(Msg("Confirm", player.UserIDString, Mathf.RoundToInt(config.ConfirmSeconds), ShortName(vehicle)));
                    return;
                }
            }

            pendingConfirm.Remove(player.userID.Get());
            cooldowns[player.userID.Get()] = now + config.CooldownSeconds;

            vehicle.Kill();
            GiveRefund(player);
        }

        private BaseVehicle FindVehicle(BasePlayer player)
        {
            RaycastHit[] hits = Physics.RaycastAll(player.eyes.position, player.eyes.HeadForward(), config.Range);
            if (hits == null || hits.Length == 0) return null;

            BaseVehicle best = null;
            float bestDistance = float.MaxValue;

            foreach (var hit in hits)
            {
                var entity = hit.GetEntity();
                var vehicle = entity as BaseVehicle;
                if (vehicle == null || vehicle.IsDestroyed) continue;

                if (hit.distance < bestDistance)
                {
                    bestDistance = hit.distance;
                    best = vehicle;
                }
            }

            return best;
        }

        private bool IsOccupied(BaseVehicle vehicle)
        {
            // Any mounted player sits inside the vehicle hierarchy (seats are children).
            foreach (var child in vehicle.children)
            {
                var mounted = child as BasePlayer;
                if (mounted != null && mounted.isMounted) return true;

                var mountable = child as BaseMountable;
                if (mountable != null && mountable.GetMounted() != null) return true;
            }

            return false;
        }

        private void GiveRefund(BasePlayer player)
        {
            var item = ItemManager.CreateByName(config.RefundShortname, config.RefundAmount, 0);
            if (item == null)
            {
                player.ChatMessage(Msg("RefundFailed", player.UserIDString));
                return;
            }

            if (player.inventory.GiveItem(item))
            {
                player.ChatMessage(Msg("Refunded", player.UserIDString, item.amount, item.info.displayName.english));
                return;
            }

            item.Drop(player.transform.position + Vector3.up, Vector3.zero);
            player.ChatMessage(Msg("Refunded", player.UserIDString, item.amount, item.info.displayName.english));
        }

        private static string ShortName(BaseVehicle vehicle)
        {
            if (vehicle is BaseHelicopter) return "minicopter";
            if (vehicle is ModularCar) return "car";
            if (vehicle is BaseBoat) return "boat";
            return vehicle.ShortPrefabName ?? "vehicle";
        }

        private void Unload()
        {
            cooldowns.Clear();
            pendingConfirm.Clear();
        }
    }
}

Hooks used: OnServerInitialized

Config file

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

VehicleDespawn.json
{
  "Range (metres)": 5,
  "Refund item shortname": "scrap",
  "Refund amount": 100,
  "Cooldown (seconds)": 300,
  "Require confirmation": true,
  "Confirmation window (seconds)": 15
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/despawnRemoves the vehicle you are looking at if you own it and nobody is mounted, then gives you the configured refund item.vehicledespawn.use
Permissions
PermissionGrants
vehicledespawn.useGrants access to the /despawn command.

How the code works

  1. Find the vehicle the player is looking at

    The command builds Physics.RaycastAll from player.eyes.position along player.eyes.HeadForward() up to config.Range. Because RaycastAll returns hits in no guaranteed order, the results are filtered with hit.GetEntity() cast to BaseVehicle and the closest surviving hit wins, tracked via hit.distance. Using GetEntity() also resolves colliders owned by child objects of the vehicle hierarchy, so a truck bed or door counts as the car.

  2. Check ownership, permission and mounted players

    Access is player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermUse). Ownership is vehicle.OwnerID == player.userID.Get() and is skipped for admins. IsOccupied walks vehicle.children and returns true when a child is a BasePlayer that reports isMounted, or a BaseMountable whose GetMounted() is non-null. That loop catches seats that do not sit directly on the vehicle object.

  3. Send the refund item safely

    The refund is built with ItemManager.CreateByName(config.RefundShortname, config.RefundAmount, 0). If that returns null the message RefundFailed is sent instead of a crash. Otherwise player.inventory.GiveItem(item) is tried first; when it returns false the item is placed on the ground with item.Drop(player.transform.position + Vector3.up, Vector3.zero). Both branches send the same Refunded message using item.info.displayName.english and item.amount.

  4. Cooldown and confirmation

    A Dictionary<ulong, float> stores UnityEngine.Time.realtimeSinceStartup + config.CooldownSeconds per player after every successful removal. Before that, the command compares the stored value with the current time and replies with Mathf.CeilToInt of the remaining seconds. When RequireConfirmation is on, the first attempt writes now + config.ConfirmSeconds into a second dictionary and returns the Confirm message; the second attempt inside the window proceeds.

  5. Removing the vehicle

    The plugin calls vehicle.Kill() rather than Die(). Nothing inside the vehicle is moved into a crate: storage, modules and fuel go away with it. The ShortName helper maps BaseHelicopter, ModularCar and BaseBoat to a readable word for the confirmation message and falls back to ShortPrefabName. If you want fuel back, capture it from the vehicle before Kill() in your own fork.

  6. Configuration, permissions and cleanup

    Init() registers vehicledespawn.use. LoadDefaultConfig builds a PluginConfig with Range 5.0, RefundShortname scrap, RefundAmount 100, CooldownSeconds 300, RequireConfirmation true and ConfirmSeconds 15. OnServerInitialized runs ItemManager.FindItemDefinition once and logs a warning if the refund shortname is wrong; that check is startup-only, never inside a hot path. Unload clears the cooldown and pending-confirmation dictionaries.

Install it on your server

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

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

Variations

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

  • Refund as fuel remaining

    Returns low grade fuel based on how much fuel is currently in the tank instead of a flat item.

    Generate this version
  • Despawn only at a repair bench

    Requires the player to stand near a repair bench before the vehicle can be removed.

    Generate this version
  • Admin recall command

    Adds a console command that despawns any vehicle by its entity ID regardless of owner.

    Generate this version

FAQ

Will this refund the fuel inside the vehicle?

No. The plugin deletes the vehicle with Kill() and sends only the configured refund item. If you want fuel back, change the code to read the vehicle's fuel container before Kill() and move the items into the player's inventory, or use the fuel-based variation above.

What happens if the vehicle is being driven?

The command refuses. It checks every child of the vehicle for a mounted BasePlayer or a BaseMountable with a mounted occupant, so a driver or passenger blocks the despawn. Refusal text is sent through lang, so you can edit oxide/lang/en/VehicleDespawn.json to change the wording.

Can admins despawn vehicles they do not own?

Yes. When player.IsAdmin is true the ownership check is skipped, so an admin can remove a stuck vehicle to clear the map. Regular players need the vehicledespawn.use permission and must match vehicle.OwnerID.

How do I install it on Oxide or Carbon?

Drop VehicleDespawn.cs into oxide/plugins, then oxide.grant group default vehicledespawn.use so everyone can use it. Carbon loads the same file from its plugins folder. Run oxide.reload VehicleDespawn afterwards and edit oxide/config/VehicleDespawn.json to change range or refund.

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 Oxide plugin with a /despawn chat command that raycasts 5 metres, removes the BaseVehicle the player owns and is looking at when nobody is mounted, and refunds 100 scrap with a 300 second cooldown.
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 recall command

Customize