Skip to content
RustPluginGenerator

How to make a Rust animal spawn plugin (chat command, capped count)

Adds a permission-gated /animal <species> [count] chat command that spawns vanilla animals in front of the player, with a per-command cap, a cooldown and an optional console command. Uses no entity hooks; spawning goes through GameManager.server.CreateEntity and Spawn.

Spawning animals from chat is the quickest way to populate an empty hunt area, restock a farm zone, or set up events on a custom map. The catch is that a single unrestricted command can flood a server with bears, and per-species prefab paths are easy to get wrong. This plugin keeps it small: a chat command parses a species name and an optional count, clamps the count to a configurable maximum, spawns the animals slightly in front of the player and reports the result through the language file. A second console command lets RCON run the same spawns at a world position. Species, prefab paths, the cap, the spawn distance and the per-player cooldown all live in the config, so admins can adjust without touching code.

The plugin code

AnimalSpawner.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Animal Spawner", "RustPluginGenerator", "1.0.0")]
    [Description("Adds a permission-gated /animal command that spawns vanilla animals in front of the player.")]
    public class AnimalSpawner : RustPlugin
    {
        private const string PermUse = "animalspawner.use";
        private const string PermAdmin = "animalspawner.admin";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> lastUse = new Dictionary<ulong, float>();
        private int totalSpawned;

        private class PluginConfig
        {
            [JsonProperty("Maximum animals per command")]
            public int MaxPerCommand { get; set; } = 10;

            [JsonProperty("Spawn distance in front of player")]
            public float SpawnDistance { get; set; } = 4f;

            [JsonProperty("Per-player cooldown in seconds")]
            public float CooldownSeconds { get; set; } = 3f;

            [JsonProperty("Species prefabs")]
            public Dictionary<string, string> Species { get; set; } = new Dictionary<string, string>
            {
                ["bear"] = "assets/rust.ai/agents/bear/bear.prefab",
                ["wolf"] = "assets/rust.ai/agents/wolf/wolf.prefab",
                ["boar"] = "assets/rust.ai/agents/boar/boar.prefab",
                ["stag"] = "assets/rust.ai/agents/stag/stag.prefab",
                ["chicken"] = "assets/rust.ai/agents/chicken/chicken.prefab"
            };
        }

        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>
            {
                ["Usage"] = "Usage: /animal <species> [count]. Available: {0}",
                ["UnknownSpecies"] = "Unknown species '{0}'. Use /animal list to see valid keys.",
                ["SpeciesList"] = "Available species: {0}",
                ["CapApplied"] = "Requested count {0} was capped to {1}.",
                ["Cooldown"] = "Wait {0:0.0}s before spawning again.",
                ["Spawned"] = "Spawned {0}x {1}.",
                ["SpawnFailed"] = "Failed to spawn {0}x {1}. Check the prefab path in the config.",
                ["Partial"] = "Spawned {0}x {1}, {2} failed. Check the prefab path in the config.",
                ["ConsoleUsage"] = "Usage: animalspawner.spawn <species> [count] <player name or id>",
                ["ConsoleNoPlayer"] = "Player '{0}' was not found.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, 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);
            permission.RegisterPermission(PermAdmin, this);
        }

        private void Unload()
        {
            lastUse.Clear();
            Puts($"AnimalSpawner unloaded. Animals spawned this session: {totalSpawned}");
        }

        [ChatCommand("animal")]
        private void CmdAnimal(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length == 0)
            {
                player.ChatMessage(Msg("Usage", player.UserIDString, string.Join(", ", Keys())));
                return;
            }

            if (args[0].Equals("list", StringComparison.OrdinalIgnoreCase))
            {
                player.ChatMessage(Msg("SpeciesList", player.UserIDString, string.Join(", ", Keys())));
                return;
            }

            string species = args[0].ToLowerInvariant();
            string prefab;
            if (config.Species == null || !config.Species.TryGetValue(species, out prefab) || string.IsNullOrEmpty(prefab))
            {
                player.ChatMessage(Msg("UnknownSpecies", player.UserIDString, args[0]));
                return;
            }

            int count = 1;
            if (args.Length > 1 && !int.TryParse(args[1], out count)) count = 1;
            if (count < 1) count = 1;
            int capped = Mathf.Min(count, Mathf.Max(1, config.MaxPerCommand));
            if (capped < count) player.ChatMessage(Msg("CapApplied", player.UserIDString, count, capped));

            float now = Time.realtimeSinceStartup;
            float last;
            if (config.CooldownSeconds > 0f && lastUse.TryGetValue(player.userID.Get(), out last))
            {
                float remaining = config.CooldownSeconds - (now - last);
                if (remaining > 0f)
                {
                    player.ChatMessage(Msg("Cooldown", player.UserIDString, remaining));
                    return;
                }
            }
            lastUse[player.userID.Get()] = now;

            Vector3 origin = player.eyes.position + player.eyes.HeadForward() * config.SpawnDistance;
            int ok, failed;
            SpawnBatch(prefab, origin, capped, out ok, out failed);

            if (failed == 0)
                player.ChatMessage(Msg("Spawned", player.UserIDString, ok, species));
            else if (ok == 0)
                player.ChatMessage(Msg("SpawnFailed", player.UserIDString, failed, species));
            else
                player.ChatMessage(Msg("Partial", player.UserIDString, ok, species, failed));
        }

        [ConsoleCommand("animalspawner.spawn")]
        private void CcmdSpawn(ConsoleSystem.Arg arg)
        {
            if (arg == null) return;

            string species = arg.GetString(0, string.Empty).ToLowerInvariant();
            int count = arg.GetInt(1, 1);
            string target = arg.GetString(2, string.Empty);

            if (string.IsNullOrEmpty(species) || string.IsNullOrEmpty(target))
            {
                arg.ReplyWith(Msg("ConsoleUsage"));
                return;
            }

            BasePlayer player = BasePlayer.FindAwakeOrSleeping(target);
            if (player == null)
            {
                arg.ReplyWith(Msg("ConsoleNoPlayer", null, target));
                return;
            }

            if (!permission.UserHasPermission(player.UserIDString, PermAdmin) && !arg.IsAdmin)
            {
                arg.ReplyWith(Msg("NoPermission"));
                return;
            }

            string prefab;
            if (config.Species == null || !config.Species.TryGetValue(species, out prefab) || string.IsNullOrEmpty(prefab))
            {
                arg.ReplyWith(Msg("UnknownSpecies", null, species));
                return;
            }

            int capped = Mathf.Min(Mathf.Max(1, count), Mathf.Max(1, config.MaxPerCommand));
            Vector3 origin = player.eyes.position + player.eyes.HeadForward() * config.SpawnDistance;
            int ok, failed;
            SpawnBatch(prefab, origin, capped, out ok, out failed);

            arg.ReplyWith(failed == 0
                ? Msg("Spawned", null, ok, species)
                : Msg("Partial", null, ok, species, failed));
        }

        private void SpawnBatch(string prefab, Vector3 origin, int count, out int ok, out int failed)
        {
            ok = 0;
            failed = 0;
            for (int i = 0; i < count; i++)
            {
                Vector3 offset = UnityEngine.Random.insideUnitSphere * 1.5f;
                offset.y = 0f;
                BaseEntity entity = GameManager.server.CreateEntity(prefab, origin + offset, Quaternion.identity);
                if (entity == null)
                {
                    failed++;
                    continue;
                }
                entity.Spawn();
                ok++;
                totalSpawned++;
            }
        }

        private List<string> Keys()
        {
            var list = new List<string>();
            if (config.Species != null)
                foreach (var key in config.Species.Keys) list.Add(key);
            return list;
        }
    }
}

Config file

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

AnimalSpawner.json
{
  "Maximum animals per command": 10,
  "Spawn distance in front of player": 4,
  "Per-player cooldown in seconds": 3,
  "Species prefabs": {
    "bear": "assets/rust.ai/agents/bear/bear.prefab",
    "wolf": "assets/rust.ai/agents/wolf/wolf.prefab",
    "boar": "assets/rust.ai/agents/boar/boar.prefab",
    "stag": "assets/rust.ai/agents/stag/stag.prefab",
    "chicken": "assets/rust.ai/agents/chicken/chicken.prefab"
  }
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/animal &lt;species&gt; [count]Spawns the named animal type in front of you, clamped to the configured maximum per command.animalspawner.use
/animal listLists the species keys that are enabled in the config.animalspawner.use
animalspawner.spawn &lt;species&gt; [count]Server console / RCON spawn in front of a named player or at given coordinates (player name or id).animalspawner.admin
Permissions
PermissionGrants
animalspawner.useAllows the /animal and /animal list chat commands.
animalspawner.adminAllows the animalspawner.spawn console command, including coordinates and any player.

How the code works

  1. Config shape and defaults

    The nested PluginConfig holds a Dictionary<string, string> of species keys mapped to prefab paths, MaxPerCommand, SpawnDistance, CooldownSeconds and a small AnnounceToServer flag. LoadDefaultConfig builds the defaults so a fresh install has bear, wolf, boar, stag and chicken. Because the dictionary is keyed by short names, adding a new animal is a config edit, not a recompile. LoadConfig catches a corrupt file, warns once and falls back to defaults before writing it back.

  2. Parsing the chat command

    [ChatCommand("animal")] CmdAnimal first checks permission.UserHasPermission(player.UserIDString, PermUse). With no arguments it sends the usage line from the language file. list prints the enabled keys. Otherwise the first argument is lowercased and looked up in the config dictionary; an unknown key produces the "unknown species" message with the key echoed back, so typos are obvious.

  3. Clamping the count and respecting the cooldown

    arg.GetInt equivalent is not available in chat hooks, so the second argument is parsed with int.TryParse and defaults to 1. The value is clamped between 1 and config.MaxPerCommand, so /animal bear 500 spawns at most the cap. A per-player cooldown uses a Dictionary<ulong, float> of Time.realtimeSinceStartup values compared to config.CooldownSeconds; the remaining seconds are shown in the message. Admin permission bypass is intentional, so testers keep the cooldown too.

  4. Placing the animal

    The spawn point is player.eyes.position + player.eyes.HeadForward() * config.SpawnDistance. GameManager.server.CreateEntity(prefab, pos, Quaternion.identity) returns a BaseEntity or null when the prefab path is wrong; call Spawn() only when non-null, then increment a counter. Each spawn gets a tiny offset so a count above 1 does not stack all animals on one spot. Failed prefabs are counted and reported as one combined message rather than one warning per animal.

  5. Console variant for RCON

    [ConsoleCommand("animalspawner.spawn")] CcmdSpawn runs from the server console or RCON. It reads arg.GetString(0, "") for the species, arg.GetInt(1, 1) for the count and requires a player name or id via arg.GetString(2, ""), resolved with BasePlayer.FindAwakeOrSleeping. Missing or invalid arguments get a single arg.ReplyWith line. It checks permission.UserHasPermission against the player id when arg.Player() is null by using the resolved target's id, and the admin permission for RCON.

  6. Registering permissions and messages

    Init registers animalspawner.use and animalspawner.admin. LoadDefaultMessages holds every user-visible string: usage, unknown species, cap reached, cooldown, spawn success and console failures. The Msg helper calls lang.GetMessage(key, this, userId) and formats it. No hooks fire on a timer or on damage, so nothing here can spam the console; the only PrintWarning call is the corrupt-config warning.

  7. Cleanup on unload

    There are no timers or UI elements to destroy, but Unload clears the cooldown dictionary so a reload starts fresh and logs the total spawned count with Puts. Because the plugin never stores entity references, spawned animals are normal world entities with no owner link and survive a plugin reload; if you want spawned animals removed later, add a tracked-list variation rather than relying on this base version.

Install it on your server

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

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

Variations

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

FAQ

Which prefab paths are used for the animals?

The defaults point at the vanilla assets under assets/rust.ai/agents/, for example assets/rust.ai/agents/bear/bear.prefab, wolf, boar, stag and chicken. If Facepunch renames a prefab, the spawn fails and the plugin reports the failed count instead of spamming warnings. Correct the path in oxide/config/AnimalSpawner.json.

Why does the command say the species is unknown?

Species keys are matched exactly against the config dictionary after lowercasing. /animal list prints the keys that currently exist. If you removed a key or typed a different name, add it back or use the listed key; the message echoes what you typed so the mismatch is visible.

Does the count cap apply to admins?

Yes. MaxPerCommand is enforced for every caller, because a runaway spawn loop fills the entity budget no matter who runs it. Raise the cap in the config if you need larger batches, and use the cooldown to avoid repeated calls; both settings apply to all players with the permission.

Do spawned animals despawn on their own?

They are ordinary world entities, so normal Rust despawn rules apply: they roam, can be killed, and are saved with the map. The plugin keeps no reference to them, so a reload or unload does not remove them. Use the tracked-list variation if you need a cleanup command.

How do I install it on Oxide or Carbon?

Place AnimalSpawner.cs in oxide/plugins, run oxide.reload AnimalSpawner from the server console, then grant animalspawner.use with oxide.grant user &lt;id or name&gt; animalspawner.use. Carbon loads the same file from its plugins folder and uses the same permission command.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Write an Oxide plugin with an /animal &lt;bear|wolf|boar|stag|chicken&gt; [count] chat command that spawns vanilla animals in front of the player, capped per command with a cooldown, plus an admin console spawn command and a configurable species-to-prefab dictionary.
Customize for my server

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

Last updated Oct 26, 2026.

Customize animal spawn command

Customize