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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/animal <species> [count] | Spawns the named animal type in front of you, clamped to the configured maximum per command. | animalspawner.use |
/animal list | Lists the species keys that are enabled in the config. | animalspawner.use |
animalspawner.spawn <species> [count] | Server console / RCON spawn in front of a named player or at given coordinates (player name or id). | animalspawner.admin |
| Permission | Grants |
|---|---|
animalspawner.use | Allows the /animal and /animal list chat commands. |
animalspawner.admin | Allows the animalspawner.spawn console command, including coordinates and any player. |
How the code works
Config shape and defaults
The nested
PluginConfigholds aDictionary<string, string>of species keys mapped to prefab paths,MaxPerCommand,SpawnDistance,CooldownSecondsand a smallAnnounceToServerflag.LoadDefaultConfigbuilds 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.LoadConfigcatches a corrupt file, warns once and falls back to defaults before writing it back.Parsing the chat command
[ChatCommand("animal")] CmdAnimalfirst checkspermission.UserHasPermission(player.UserIDString, PermUse). With no arguments it sends the usage line from the language file.listprints 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.Clamping the count and respecting the cooldown
arg.GetIntequivalent is not available in chat hooks, so the second argument is parsed withint.TryParseand defaults to 1. The value is clamped between 1 andconfig.MaxPerCommand, so/animal bear 500spawns at most the cap. A per-player cooldown uses aDictionary<ulong, float>ofTime.realtimeSinceStartupvalues compared toconfig.CooldownSeconds; the remaining seconds are shown in the message. Admin permission bypass is intentional, so testers keep the cooldown too.Placing the animal
The spawn point is
player.eyes.position + player.eyes.HeadForward() * config.SpawnDistance.GameManager.server.CreateEntity(prefab, pos, Quaternion.identity)returns aBaseEntityor null when the prefab path is wrong; callSpawn()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.Console variant for RCON
[ConsoleCommand("animalspawner.spawn")] CcmdSpawnruns from the server console or RCON. It readsarg.GetString(0, "")for the species,arg.GetInt(1, 1)for the count and requires a player name or id viaarg.GetString(2, ""), resolved withBasePlayer.FindAwakeOrSleeping. Missing or invalid arguments get a singlearg.ReplyWithline. It checkspermission.UserHasPermissionagainst the player id whenarg.Player()is null by using the resolved target's id, and the admin permission for RCON.Registering permissions and messages
Initregistersanimalspawner.useandanimalspawner.admin.LoadDefaultMessagesholds every user-visible string: usage, unknown species, cap reached, cooldown, spawn success and console failures. TheMsghelper callslang.GetMessage(key, this, userId)and formats it. No hooks fire on a timer or on damage, so nothing here can spam the console; the onlyPrintWarningcall is the corrupt-config warning.Cleanup on unload
There are no timers or UI elements to destroy, but
Unloadclears the cooldown dictionary so a reload starts fresh and logs the total spawned count withPuts. 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
Save AnimalSpawner.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/AnimalSpawner.json(Carbon:carbon/configs/AnimalSpawner.json). After editing, reload it:oxide.reload AnimalSpawner c.reload AnimalSpawner # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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.
Track and despawn
Store spawned entity IDs so admins can wipe everything the plugin created.
Generate this versionBiome-aware placement
Reject spawns in the wrong biome for the species.
Generate this versionRandom herd spawns
Spawn a small group with scatter instead of a line.
Generate this version
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 <id or name> 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More npcs plugins
- Custom NPC lootRewrites the items inside corpses left by configured NPCs, using a weighted per-prefab loot table.OnServerInitializedOnCorpsePopulate
- NPC free zonesDefines named world zones where newly spawned scientists and animals are deleted immediately.OnEntitySpawned
- NPC damage multiplierScales damage NPCs deal to players and damage players deal to NPCs independently, with per-NPC-type overrides, using Oxide'sโฆOnEntityTakeDamage
Last updated Oct 26, 2026.
Customize animal spawn command
Customize