Skip to content
RustPluginGenerator

OnEntitySpawned

OnEntitySpawned is called after BaseNetworkable.Spawn completes server initialization for an entity, right before its first immediate network update is sent. It fires for every networked entity on the server, including players, deployables, and map-spawned objects.

Signature
void OnEntitySpawned(BaseNetworkable entity)

Parameters

Hook parameters
ParameterTypeDescription
entityBaseNetworkableThe BaseNetworkable that just finished spawning. Cast it to a concrete type (BasePlayer, BuildingBlock, StorageContainer, etc.) or inspect ShortPrefabName to decide whether the spawn is interesting.

Return value

The return value is ignored. BaseNetworkable.Spawn is void and continues with SendNetworkUpdateImmediate and its remaining steps regardless of what your method returns.

How it works

The hook fires inside BaseNetworkable.Spawn, after ServerInit, PostInitShared, network group assignment and the isSpawned flag, but before SendNetworkUpdateImmediate. Because players, NPCs, deployables, dropped items, vehicles and map entities all go through this path, it fires very frequently on a busy server.

Use the entity argument to filter by type: cast with as BasePlayer, as BuildingBlock, as StorageContainer, as BaseOven and so on. entity.ShortPrefabName and entity.PrefabName are cheap string checks for prefab-specific logic. entity.net.ID.Value gives you a stable network id for tracking.

Declaring a more specific parameter type (for example BasePlayer) narrows dispatch: Oxide skips your method when the spawned object is not an instance of that type. This is the cheapest way to only react to players or to a specific entity class.

Because it fires so often, avoid heavy work here. Do not call Puts, PrintWarning, or network/UI updates unconditionally. Instead, set a flag, queue the entity id, or schedule work with NextTick or a short timer. Entities removed in the same frame may still trigger this hook, so null-check and verify IsDestroyed before acting.

Example plugin: Limit deployables of a prefab per player

SpawnLimitGuard.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("SpawnLimitGuard", "RustPluginGenerator", "1.0.0")]
    [Description("Enforces per-player deployable limits for configured prefabs")]
    public class SpawnLimitGuard : RustPlugin
    {
        private const string PermUse = "spawnlimitguard.use";

        private PluginConfig config;
        private readonly Dictionary<string, int> limits = new Dictionary<string, int>();

        private class PluginConfig
        {
            [JsonProperty("Enable deployable limit checks")]
            public bool Enabled = true;

            [JsonProperty("Prefab limits (ShortPrefabName:MaxPerPlayer)")]
            public Dictionary<string, int> PrefabLimits = new Dictionary<string, int>
            {
                ["furnace"] = 1,
                ["furnace.large"] = 1,
                ["small_storage_box"] = 3,
                ["autoturret_deployed"] = 1
            };

            [JsonProperty("Use permission")]
            public bool UsePermission = true;
        }

        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>
            {
                ["LimitReached"] = "You already have the maximum number of {0} ({1})."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
            => string.Format(lang.GetMessage(key, this, userId), args);

        private void Init()
        {
            permission.RegisterPermission(PermUse, this);
            limits.Clear();
            foreach (var entry in config.PrefabLimits)
            {
                if (string.IsNullOrEmpty(entry.Key) || entry.Value <= 0) continue;
                limits[entry.Key] = entry.Value;
            }
        }

        private void OnEntitySpawned(BaseNetworkable entity)
        {
            if (!config.Enabled || entity == null) return;

            var baseEntity = entity as BaseEntity;
            if (baseEntity == null || baseEntity.OwnerID == 0) return;

            var prefab = entity.ShortPrefabName;
            if (prefab == null) return;

            int max;
            if (!limits.TryGetValue(prefab, out max)) return;

            var owner = BasePlayer.FindByID(baseEntity.OwnerID);
            if (owner == null || owner.IsNpc) return;
            if (config.UsePermission && !permission.UserHasPermission(owner.UserIDString, PermUse)) return;

            int count = 0;
            foreach (var other in BaseNetworkable.serverEntities)
            {
                var candidate = other as BaseEntity;
                if (candidate == null || candidate == baseEntity) continue;
                if (candidate.OwnerID != baseEntity.OwnerID) continue;
                if (candidate.ShortPrefabName != prefab) continue;
                count++;
            }

            if (count < max) return;

            var entityToKill = baseEntity;
            var player = owner;
            var limit = max;
            var name = prefab;
            NextTick(() =>
            {
                if (entityToKill != null && !entityToKill.IsDestroyed)
                {
                    entityToKill.Kill();
                }
                if (player != null && player.IsConnected)
                {
                    player.ChatMessage(Msg("LimitReached", player.UserIDString, name, limit));
                }
            });
        }
    }
}

The plugin declares the hook with BaseNetworkable and immediately filters to BaseEntity so it only considers world entities, skipping items and other networkables. The OwnerID from the entity identifies the player who placed it, so the check works for deployables like furnaces and boxes. A config list of Prefab:Max pairs is parsed at load time into the tracked prefabs, and CheckLimit activates only when the entity's ShortPrefabName matches a tracked rule and the player has permission. When the count exceeds the limit, the plugin schedules a next-tick kill and sends a chat warning through the language file. Untracked prefabs exit immediately, keeping the per-spawn cost to a single dictionary lookup.

Common use cases

  • Cap deployables per player

    Count existing entities by prefab and OwnerID, and remove new ones that exceed a configured limit. Useful for furnace, box or turret caps on small servers.

  • React to specific map entities

    Detect a supply drop, Bradley APC or Patrol Helicopter by ShortPrefabName and start a tracking timer, broadcast a warning, or register the entity id for later lookup.

  • Auto-configure new containers

    When a StorageContainer spawns as an owned deployable, apply a skin, lock the loot panel, or tag the entity id in a data file so admins can review newly placed storage.

Pitfalls

  • Fires for almost everything

    Players, NPCs, dropped items, loot and map props all hit this hook. Without an early type or prefab filter, your callback runs hundreds of times per minute and costs CPU.

  • Killing too early

    Calling Kill() directly inside the hook can leave a partially networked object behind. Schedule the kill with NextTick or timer.Once so the entity finishes its spawn sequence first.

  • OwnerID is not always a player

    Map-spawned objects and entities owned by the server can have an OwnerID that does not map to a connected player. Resolve BasePlayer.FindByID and handle a null result before sending messages.

Plugin recipes that use OnEntitySpawned

Build a plugin with OnEntitySpawned

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Cap deployables per player using the OnEntitySpawned hook, with a config file and a permission
Generate this plugin

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

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnEntitySpawned

Generate