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
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.
{
"Range (metres)": 5,
"Refund item shortname": "scrap",
"Refund amount": 100,
"Cooldown (seconds)": 300,
"Require confirmation": true,
"Confirmation window (seconds)": 15
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/despawn | Removes the vehicle you are looking at if you own it and nobody is mounted, then gives you the configured refund item. | vehicledespawn.use |
| Permission | Grants |
|---|---|
vehicledespawn.use | Grants access to the /despawn command. |
How the code works
Find the vehicle the player is looking at
The command builds
Physics.RaycastAllfromplayer.eyes.positionalongplayer.eyes.HeadForward()up toconfig.Range. Because RaycastAll returns hits in no guaranteed order, the results are filtered withhit.GetEntity()cast toBaseVehicleand the closest surviving hit wins, tracked viahit.distance. UsingGetEntity()also resolves colliders owned by child objects of the vehicle hierarchy, so a truck bed or door counts as the car.Check ownership, permission and mounted players
Access is
player.IsAdmin || permission.UserHasPermission(player.UserIDString, PermUse). Ownership isvehicle.OwnerID == player.userID.Get()and is skipped for admins.IsOccupiedwalksvehicle.childrenand returns true when a child is aBasePlayerthat reportsisMounted, or aBaseMountablewhoseGetMounted()is non-null. That loop catches seats that do not sit directly on the vehicle object.Send the refund item safely
The refund is built with
ItemManager.CreateByName(config.RefundShortname, config.RefundAmount, 0). If that returns null the messageRefundFailedis sent instead of a crash. Otherwiseplayer.inventory.GiveItem(item)is tried first; when it returns false the item is placed on the ground withitem.Drop(player.transform.position + Vector3.up, Vector3.zero). Both branches send the sameRefundedmessage usingitem.info.displayName.englishanditem.amount.Cooldown and confirmation
A
Dictionary<ulong, float>storesUnityEngine.Time.realtimeSinceStartup + config.CooldownSecondsper player after every successful removal. Before that, the command compares the stored value with the current time and replies withMathf.CeilToIntof the remaining seconds. WhenRequireConfirmationis on, the first attempt writesnow + config.ConfirmSecondsinto a second dictionary and returns theConfirmmessage; the second attempt inside the window proceeds.Removing the vehicle
The plugin calls
vehicle.Kill()rather thanDie(). Nothing inside the vehicle is moved into a crate: storage, modules and fuel go away with it. TheShortNamehelper mapsBaseHelicopter,ModularCarandBaseBoatto a readable word for the confirmation message and falls back toShortPrefabName. If you want fuel back, capture it from the vehicle beforeKill()in your own fork.Configuration, permissions and cleanup
Init()registersvehicledespawn.use.LoadDefaultConfigbuilds aPluginConfigwithRange5.0,RefundShortnamescrap,RefundAmount100,CooldownSeconds300,RequireConfirmationtrue andConfirmSeconds15.OnServerInitializedrunsItemManager.FindItemDefinitiononce and logs a warning if the refund shortname is wrong; that check is startup-only, never inside a hot path.Unloadclears the cooldown and pending-confirmation dictionaries.
Install it on your server
Save VehicleDespawn.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/VehicleDespawn.json(Carbon:carbon/configs/VehicleDespawn.json). After editing, reload it:oxide.reload VehicleDespawn c.reload VehicleDespawn # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionDespawn only at a repair bench
Requires the player to stand near a repair bench before the vehicle can be removed.
Generate this versionAdmin 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.
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 recall command
Customize