Skip to content
RustPluginGenerator

OnNpcDuck

OnNpcDuck is called from HumanNPC.SetDucked right before the NPC's ducked model state is written and sent to clients. Returning a non-null value cancels the ducking change entirely.

Signature
object OnNpcDuck(HumanNPC humanNPC)

Parameters

Hook parameters
ParameterTypeDescription
humanNPCHumanNPCThe HumanNPC instance whose duck state is about to change. It carries net.ID, position, model state and the AI brain, so you can filter by prefab, location or identifier before allowing the change.

Return value

Return null (or let the method fall through) to allow the NPC's ducked state to be written and networked normally. Return any non-null value to suppress modelState.ducked = flag and the following SendNetworkUpdate(), so the NPC keeps its previous stance. For a non-null return you can return any object; the value itself is not used.

How it works

The hook fires inside HumanNPC.SetDucked(bool flag), which is the single method that writes modelState.ducked on a humanoid NPC and pushes a network update. Anything that routes through that method triggers the hook: AI brain decisions that make a scientist crouch behind cover, scripted behaviour on custom NPCs, and plugin code that calls SetDucked directly. The flag argument is not exposed to the hook, so you cannot read from the hook whether the NPC is ducking or standing up; you can only inspect humanNPC.modelState.ducked for the current (pre-change) value.

The humanNPC parameter is typed HumanNPC, so Oxide only dispatches the hook for NPCs that are actually HumanNPC instances. Declaring a more specific type such as ScientistNPC narrows the hook to scientists, and anything else is skipped before your method runs. Default scientist NPCs and custom humanoid NPCs both qualify.

Because this hook fires on AI movement decisions, it can run frequently on a populated server. Keep the body cheap: no printing, no entity scans, no data writes. Cancelling the change leaves the NPC's model state untouched.

Example plugin: Keep tagged guard NPCs standing upright

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

namespace Oxide.Plugins
{
    [Info("NpcDuckGuard", "RustPluginGenerator", "1.0.0")]
    [Description("Prevents admin-tagged NPCs from crouching.")]
    public class NpcDuckGuard : RustPlugin
    {
        private const string PermUse = "npcduckguard.use";
        private const string DataFile = "NpcDuckGuard";

        private StoredData data;
        private readonly HashSet<ulong> lockedNpcIds = new HashSet<ulong>();

        private class StoredData
        {
            public List<ulong> LockedNpcIds = new List<ulong>();
        }

        private class PluginConfig
        {
            [JsonProperty("Maximum lock distance (metres)")]
            public float MaxLockDistance = 10f;
        }

        private PluginConfig config;

        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>
            {
                ["NoTarget"] = "Look at an NPC within {0} metres.",
                ["Locked"] = "NPC {0} will no longer crouch.",
                ["Unlocked"] = "NPC {0 can crouch again.".Replace("{0", "{0}"),
                ["NotLocked"] = "NPC {0} is not locked.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, 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);
            LoadData();
        }

        private void LoadData()
        {
            data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataFile) ?? new StoredData();
            lockedNpcIds.Clear();
            foreach (var id in data.LockedNpcIds)
                lockedNpcIds.Add(id);
        }

        private void SaveData()
        {
            data.LockedNpcIds.Clear();
            data.LockedNpcIds.AddRange(lockedNpcIds);
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, data);
        }

        private object OnNpcDuck(HumanNPC humanNPC)
        {
            if (humanNPC == null || humanNPC.net == null) return null;
            return lockedNpcIds.Contains(humanNPC.net.ID.Value) ? (object)true : null;
        }

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

            var npc = FindAimedNpc(player);
            if (npc == null)
            {
                SendReply(player, Msg("NoTarget", player.UserIDString, config.MaxLockDistance));
                return;
            }

            ulong id = npc.net.ID.Value;
            if (lockedNpcIds.Remove(id))
            {
                SaveData();
                SendReply(player, Msg("Unlocked", player.UserIDString, id));
                return;
            }

            lockedNpcIds.Add(id);
            SaveData();
            SendReply(player, Msg("Locked", player.UserIDString, id));
        }

        private HumanNPC FindAimedNpc(BasePlayer player)
        {
            RaycastHit hit;
            if (!Physics.Raycast(player.eyes.HeadRay(), out hit, config.MaxLockDistance)) return null;
            var entity = hit.GetEntity() as HumanNPC;
            return entity != null && !entity.IsDestroyed ? entity : null;
        }

        private void Unload()
        {
            SaveData();
        }
    }
}

The plugin manages a saved list of NPC network IDs whose owners have locked them into a standing stance. /npdlock is an admin-only chat command: the admin aims at an NPC (the target under the crosshair) and the plugin records its net.ID.Value. From then on, OnNpcDuck checks the incoming HumanNPC against that set and returns a non-null value for locked entries, so SetDucked never writes the ducked flag and the NPC stays upright.

The set is persisted with Interface.Oxide.DataFileSystem in oxide/data/NpcDuckGuard and loaded on startup. Stale IDs are harmless: if the entity no longer exists the hook simply never matches it. The lock list is kept in a HashSet<ulong> so the lookup inside the hook is constant time and adds no measurable load.

Common use cases

  • Fixed-pose display NPCs

    Market or event NPCs that should always stand in the same pose can be blocked from ducking so ambient AI or nearby combat cannot change their stance mid-event.

  • Consistent hitbox for turrets

    Crouching NPCs present a different silhouette; if your custom NPCs are meant to be shot by AutoTurrets or Samsites you can keep them standing so targeting geometry stays predictable.

  • Debugging AI stance changes

    Temporarily block ducking on a test NPC to verify whether a stance change comes from the brain or from plugin code calling SetDucked.

Pitfalls

  • The duck direction is not passed in

    The hook only receives the NPC. The flag value from SetDucked is not forwarded, so if you need to know whether the NPC is about to crouch or stand up, read humanNPC.modelState.ducked before deciding.

  • Cancelling skips the network update

    Returning non-null also skips SendNetworkUpdate(). If a client somehow shows the old stance or a plugin set model state manually, call humanNPC.SendNetworkUpdate() yourself after forcing the desired value.

  • Runs on AI decisions

    Scientists crouch often during combat, so this hook can fire many times per second across the server. Avoid logging, LINQ allocation or permission checks inside it; precompute whatever you need at load or command time.

Build a plugin with OnNpcDuck

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

Prompt: Fixed-pose display NPCs using the OnNpcDuck 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 OnNpcDuck

Generate