Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/horseSpawns a ridable horse next to you and removes the horse you spawned previously.horsespawn.spawn
horsespawn.clearcooldownsClears every stored cooldown so players can spawn again immediately (server console/RCON).horsespawn.spawn
Permissions
PermissionGrants
horsespawn.spawnAllows using /horse and the horsespawn.clearcooldowns console command.

How the code works

  1. Config and data

    PluginConfig holds Horse Prefab (default assets/rust.ai/agents/horse/horse.prefab), Cooldown Seconds (300), Allow In Building Blocked (false), Spawn Offset (1.5) and Announce Cooldown (true), all with [JsonProperty] names. LoadConfig falls back to defaults on a broken file and SaveConfig writes it back. A small StoredData class holds a Horses dictionary mapping player IDs to entity IDs and lives in oxide/data/HorseSpawn, loaded in OnServerInitialized and saved on every change and in Unload.

  2. The /horse command

    [ChatCommand("horse")] CmdHorse(BasePlayer player, string command, string[] args) first checks permission.UserHasPermission(player.UserIDString, PermSpawn), then the in-memory cooldown dictionary keyed by player.UserIDString. If a cooldown is running it replies with the remaining seconds via the Cooldown message. Otherwise it calls SpawnHorse, sets cooldowns[id] = Time.realtimeSinceStartup + config.CooldownSeconds and answers with the Spawned message.

  3. Spawn placement

    SpawnHorse uses player.IsBuildingBlocked() unless the config allows it, then computes a position with player.transform.position + player.eyes.HeadForward() * config.SpawnOffset. The prefab comes from GameManager.server.CreateEntity(config.HorsePrefab, pos, rot). If the entity is null the player gets the SpawnFailed message. Spawn() is called on the result and the entity ID is stored with net.ID.Value before saving data.

  4. Replacing the previous horse

    Before creating a new horse, SpawnHorse looks up the stored ID for that player in data.Horses. Instead of a direct entity lookup the plugin scans BaseNetworkable.serverEntities for a matching net.ID.Value and calls Kill() 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.

  5. Forgetting horses through OnEntityKill

    object OnEntityKill(BaseNetworkable entity) runs whenever any networkable is destroyed. It null-checks entity and entity.net, then searches data.Horses for 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.

  6. Cooldowns and cleanup

    Cooldowns live in a Dictionary<string, float> with Time.realtimeSinceStartup timestamps; entries are dropped lazily when a player uses the command after the timer expired. [ConsoleCommand("horsespawn.clearcooldowns")] accepts a ConsoleSystem.Arg, checks arg.Player() for the permission when not run from console, and clears the dictionary. Unload saves data. No timers are used, so nothing needs destroying.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Scrap cost per horse

    Charge scrap from the player's inventory instead of using a pure timer cooldown.

    Generate this version
  • Time-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.

Prompt: Create a Rust Oxide plugin named HorseSpawn with a /horse chat command that spawns a ridable horse next to the player, removes that player's previous horse, uses a 300 second cooldown, a horsespawn.spawn permission, config for prefab and cooldown, and data file storage.
Customize for my server

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026.

Customize horse spawn command

Customize