How to make a Rust horse spawn plugin (Oxide/Carbon cooldown)
Adds a /horse command that spawns a ridable horse beside the player, tracks one horse per player, removes that player's previous horse and enforces a permission-gated cooldown. It uses OnEntityKill to forget horses that were destroyed by other means.
Admins who want easy horse access on a server usually end up handing out mounts manually or reading logs to find out who summoned what. A command-based spawn fixes that: /horse creates a ridable horse at the player's feet, the plugin remembers it in oxide/data, and the next use of the command despawns that player's old horse first so mounts do not pile up across the map.
The plugin below registers the horsespawn.spawn permission, reads a cooldown and an entity prefab from config, refuses to run in building blocked zones unless told otherwise, and keeps a per-player data file of the horse entity ID. Cooldowns are tracked per player in memory, and the OnEntityKill hook clears the stored ID when the horse dies or is removed some other way, so stale entries never block the next spawn. Every player-facing line comes from the language file.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Horse Spawn", "RustPluginGenerator", "1.0.0")]
[Description("Spawns a ridable horse next to the player with a permission-gated cooldown.")]
public class HorseSpawn : RustPlugin
{
private const string PermSpawn = "horsespawn.spawn";
private const string DataFileName = "HorseSpawn";
private PluginConfig config;
private StoredData data;
private readonly Dictionary<string, float> cooldowns = new Dictionary<string, float>();
private class PluginConfig
{
[JsonProperty("Horse Prefab")]
public string HorsePrefab = "assets/rust.ai/agents/horse/horse.prefab";
[JsonProperty("Cooldown Seconds")]
public float CooldownSeconds = 300f;
[JsonProperty("Allow In Building Blocked")]
public bool AllowInBuildingBlocked = false;
[JsonProperty("Spawn Offset")]
public float SpawnOffset = 1.5f;
[JsonProperty("Announce Cooldown")]
public bool AnnounceCooldown = true;
}
private class StoredData
{
[JsonProperty("Horses")]
public Dictionary<string, ulong> Horses = new Dictionary<string, ulong>();
}
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>
{
["Spawned"] = "Your horse has been spawned.",
["SpawnFailed"] = "The horse could not be spawned. Contact an admin.",
["Cooldown"] = "You can spawn another horse in {0} seconds.",
["BuildingBlocked"] = "You cannot spawn a horse in a building blocked zone.",
["NoPermission"] = "You do not have permission to spawn a horse.",
["Removed"] = "Your previous horse was removed and a new one spawned.",
["CooldownsCleared"] = "All horse cooldowns were cleared."
}, 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(PermSpawn, this);
}
private void OnServerInitialized()
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataFileName);
if (data == null || data.Horses == null) data = new StoredData();
}
private void Unload()
{
SaveData();
}
private void SaveData()
{
if (data != null) Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
}
[ChatCommand("horse")]
private void CmdHorse(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermSpawn))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
string id = player.UserIDString;
float readyAt;
if (cooldowns.TryGetValue(id, out readyAt))
{
if (readyAt <= Time.realtimeSinceStartup)
{
cooldowns.Remove(id);
}
else if (config.AnnounceCooldown)
{
player.ChatMessage(Msg("Cooldown", id, Mathf.CeilToInt(readyAt - Time.realtimeSinceStartup)));
return;
}
}
if (!SpawnHorse(player)) return;
cooldowns[id] = Time.realtimeSinceStartup + Mathf.Max(0f, config.CooldownSeconds);
}
private bool SpawnHorse(BasePlayer player)
{
string id = player.UserIDString;
if (!config.AllowInBuildingBlocked && player.IsBuildingBlocked())
{
player.ChatMessage(Msg("BuildingBlocked", id));
return false;
}
bool replaced = RemoveStoredHorse(id);
Vector3 forward = player.eyes.HeadForward();
Vector3 pos = player.transform.position + forward * config.SpawnOffset;
Quaternion rot = Quaternion.LookRotation(new Vector3(forward.x, 0f, forward.z));
BaseEntity horse = GameManager.server.CreateEntity(config.HorsePrefab, pos, rot);
if (horse == null)
{
player.ChatMessage(Msg("SpawnFailed", id));
return false;
}
horse.Spawn();
data.Horses[id] = horse.net.ID.Value;
SaveData();
player.ChatMessage(Msg(replaced ? "Removed" : "Spawned", id));
return true;
}
private bool RemoveStoredHorse(string userId)
{
ulong entityId;
if (!data.Horses.TryGetValue(userId, out entityId)) return false;
data.Horses.Remove(userId);
foreach (BaseNetworkable networkable in BaseNetworkable.serverEntities)
{
if (networkable == null || networkable.net == null) continue;
if (networkable.net.ID.Value != entityId) continue;
networkable.Kill();
return true;
}
return false;
}
private object OnEntityKill(BaseNetworkable entity)
{
if (entity == null || entity.net == null || data == null || data.Horses == null) return null;
ulong entityId = entity.net.ID.Value;
string matchedKey = null;
foreach (KeyValuePair<string, ulong> pair in data.Horses)
{
if (pair.Value != entityId) continue;
matchedKey = pair.Key;
break;
}
if (matchedKey == null) return null;
data.Horses.Remove(matchedKey);
SaveData();
return null;
}
[ConsoleCommand("horsespawn.clearcooldowns")]
private void CcmdClearCooldowns(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermSpawn))
{
arg.ReplyWith(Msg("NoPermission", player.UserIDString));
return;
}
cooldowns.Clear();
arg.ReplyWith(Msg("CooldownsCleared"));
}
}
}Hooks used: OnServerInitialized, OnEntityKill
Config file
Created automatically on first load at oxide/config/HorseSpawn.json.
{
"Horse Prefab": "assets/rust.ai/agents/horse/horse.prefab",
"Cooldown Seconds": 300,
"Allow In Building Blocked": false,
"Spawn Offset": 1.5,
"Announce Cooldown": true,
"Horses": {}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/horse | Spawns a ridable horse next to you and removes the horse you spawned previously. | horsespawn.spawn |
horsespawn.clearcooldowns | Clears every stored cooldown so players can spawn again immediately (server console/RCON). | horsespawn.spawn |
| Permission | Grants |
|---|---|
horsespawn.spawn | Allows using /horse and the horsespawn.clearcooldowns console command. |
How the code works
Config and data
PluginConfigholdsHorse Prefab(defaultassets/rust.ai/agents/horse/horse.prefab),Cooldown Seconds(300),Allow In Building Blocked(false),Spawn Offset(1.5) andAnnounce Cooldown(true), all with[JsonProperty]names.LoadConfigfalls back to defaults on a broken file andSaveConfigwrites it back. A smallStoredDataclass holds aHorsesdictionary mapping player IDs to entity IDs and lives inoxide/data/HorseSpawn, loaded inOnServerInitializedand saved on every change and inUnload.The /horse command
[ChatCommand("horse")] CmdHorse(BasePlayer player, string command, string[] args)first checkspermission.UserHasPermission(player.UserIDString, PermSpawn), then the in-memory cooldown dictionary keyed byplayer.UserIDString. If a cooldown is running it replies with the remaining seconds via theCooldownmessage. Otherwise it callsSpawnHorse, setscooldowns[id] = Time.realtimeSinceStartup + config.CooldownSecondsand answers with theSpawnedmessage.Spawn placement
SpawnHorseusesplayer.IsBuildingBlocked()unless the config allows it, then computes a position withplayer.transform.position + player.eyes.HeadForward() * config.SpawnOffset. The prefab comes fromGameManager.server.CreateEntity(config.HorsePrefab, pos, rot). If the entity is null the player gets theSpawnFailedmessage.Spawn()is called on the result and the entity ID is stored withnet.ID.Valuebefore saving data.Replacing the previous horse
Before creating a new horse,
SpawnHorselooks up the stored ID for that player indata.Horses. Instead of a direct entity lookup the plugin scansBaseNetworkable.serverEntitiesfor a matchingnet.ID.Valueand callsKill()on it when found. The entry is then overwritten with the new horse ID. This keeps exactly one plugin-spawned horse per player regardless of how far away the old one was left.Forgetting horses through OnEntityKill
object OnEntityKill(BaseNetworkable entity)runs whenever any networkable is destroyed. It null-checksentityandentity.net, then searchesdata.Horsesfor the matching ID. When a match is found the entry is removed and data is saved. The hook returns null so vanilla destruction continues normally. This is what keeps the data file from holding dead entity IDs after players kill their horse or the server despawns it.Cooldowns and cleanup
Cooldowns live in a
Dictionary<string, float>withTime.realtimeSinceStartuptimestamps; entries are dropped lazily when a player uses the command after the timer expired.[ConsoleCommand("horsespawn.clearcooldowns")]accepts aConsoleSystem.Arg, checksarg.Player()for the permission when not run from console, and clears the dictionary.Unloadsaves data. No timers are used, so nothing needs destroying.
Install it on your server
Save HorseSpawn.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/HorseSpawn.json(Carbon:carbon/configs/HorseSpawn.json). After editing, reload it:oxide.reload HorseSpawn c.reload HorseSpawn # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default horsespawn.spawn c.grant group default horsespawn.spawn # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Team-wide horses
Let a whole team share one cooldown and one mount each.
Generate this versionScrap cost per horse
Charge scrap from the player's inventory instead of using a pure timer cooldown.
Generate this versionTime-limited horses
Despawn the horse after a configured lifetime so mounts do not stay forever.
Generate this version
FAQ
Which prefab should I put in `Horse Prefab`?
The default assets/rust.ai/agents/horse/horse.prefab is the standard ridable horse. If a server build changes paths, spawn the animal once with an admin command, note PrefabName on the entity and paste the full path into the config. An invalid path returns null from GameManager.server.CreateEntity and the player sees the SpawnFailed message.
Does the plugin work if the horse is killed by a player?
Yes. OnEntityKill receives the destroyed BaseNetworkable, the plugin matches its net.ID.Value against the stored data and removes the entry, so the next /horse call does not try to kill an entity that no longer exists.
How do I install it on Oxide or Carbon?
Drop HorseSpawn.cs into oxide/plugins, run oxide.reload HorseSpawn in console, then oxide.grant group default horsespawn.spawn if everyone should use it. Carbon loads the same file from its plugins folder. The config appears in oxide/config/HorseSpawn.json after the first load.
Why does the cooldown reset after a server restart?
Cooldowns are held in memory with Time.realtimeSinceStartup, which restarts at zero when the server boots. Only the spawned horse IDs are persisted in oxide/data/HorseSpawn. If you need cooldowns to survive restarts, persist the timestamp with Facepunch.Math.Epoch.Current instead.
Can a player spawn a horse inside a base or a raid?
Not by default. Allow In Building Blocked is false, so player.IsBuildingBlocked() stops the command in blocked zones and shows the BuildingBlocked message. Set it to true if you want horses available anywhere.
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
- 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
- Unlimited vehicle fuelStops fuel drain on vehicles driven by players with a permission, limited to the vehicle types listed in config.OnFuelConsume
Last updated Oct 26, 2026.
Customize horse spawn command
Customize