Skip to content
RustPluginGenerator

OnAIBrainStateSwitch

OnAIBrainStateSwitch is called from BaseAIBrain.SwitchToState right before an NPC brain changes to a new AI state. Returning a boolean cancels the switch when you return false, or allows it normally when you return null.

Signature
object OnAIBrainStateSwitch(BaseAIBrain aIBrain, BaseAIBrain.BasicAIState CurrentState, BaseAIBrain.BasicAIState newState)

Parameters

Hook parameters
ParameterTypeDescription
aIBrainBaseAIBrainThe BaseAIBrain component that is about to change state. Read its owner (BaseAIBrain.GetEntity()) to get the NPC that drives it.
CurrentStateBaseAIBrain.BasicAIStateThe BasicAIState the brain is currently in, for example Idle, Roam, Chase, Attack or Flee. Use it to detect specific transitions.
newStateBaseAIBrain.BasicAIStateThe BasicAIState the brain wants to switch to. This is what the hook lets you block or redirect.

Return value

Returning null lets the game continue with the state switch exactly as normal. Returning a non-null value stops the original code path: the switch does not happen, SwitchToState returns false, and the brain stays in CurrentState. Any non-null object has the same effect, including true, so use false for clarity.

How it works

The hook fires inside BaseAIBrain.SwitchToState, the single method every NPC brain uses to move between states such as Idle, Roam, Chase, Attack or Flee. It runs after the game's own same-state check, so CurrentState and newState are always different in practice, and before the state change is applied. The first argument gives you the brain itself; call GetEntity() on it to reach the NPC (BaseEntity), then narrow to ScientistNPC, BaseAnimalNPC or another type to apply rules to a specific category of AI. CurrentState and newState are BaseAIBrain.BasicAIState values, which makes them useful for transition-specific logic such as "block Attack -> Chase during a PvE cooldown" or "log only Idle -> Chase entries". Because the hook fires on every state change on the server, treat it as hot code: keep the body short, avoid building strings, and never write to the console. Narrowing a parameter type (for example declaring ScientistNPC instead of BaseAIBrain) does not help here, since the first argument is always a brain component, not an entity.

Example plugin: Block NPC chase and attack during a PvE safety window

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

namespace Oxide.Plugins
{
    [Info("Npc State Freeze", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks NPC brains from entering chase or attack states during an admin-started safety window")]
    public class NpcStateFreeze : RustPlugin
    {
        private const string PermUse = "npcstatefreeze.use";

        private PluginConfig config;
        private float freezeUntil;
        private Timer freezeTimer;

        private class PluginConfig
        {
            [JsonProperty("Maximum freeze duration in seconds")]
            public float MaxDuration = 600f;

            [JsonProperty("Blocked states")]
            public string[] BlockedStates = { "Chase", "Attack" };
        }

        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>
            {
                ["NoPermission"] = "You do not have permission to use this command.",
                ["Usage"] = "Usage: /npcstatefreeze <seconds>",
                ["Started"] = "NPC aggression is frozen for {0} seconds.",
                ["Ended"] = "NPC aggression is unfrozen, brains can chase and attack again."
            }, 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);
        }

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

            float seconds;
            if (args == null || args.Length == 0 || !float.TryParse(args[0], out seconds) || seconds <= 0f)
            {
                SendReply(player, Msg("Usage", player.UserIDString));
                return;
            }

            seconds = Math.Min(seconds, config.MaxDuration);
            freezeUntil = UnityEngine.Time.realtimeSinceStartup + seconds;

            if (freezeTimer != null) freezeTimer.Destroy();
            freezeTimer = timer.Once(seconds, EndFreeze);

            Server.Broadcast(Msg("Started", null, seconds));
        }

        private void EndFreeze()
        {
            freezeUntil = 0f;
            freezeTimer = null;
            Server.Broadcast(Msg("Ended"));
        }

        private object OnAIBrainStateSwitch(BaseAIBrain aIBrain, BaseAIBrain.BasicAIState CurrentState, BaseAIBrain.BasicAIState newState)
        {
            if (freezeUntil <= 0f || UnityEngine.Time.realtimeSinceStartup >= freezeUntil) return null;
            if (aIBrain == null || aIBrain.baseEntity == null) return null;

            string stateName = newState.ToString();
            for (int i = 0; i < config.BlockedStates.Length; i++)
            {
                if (string.Equals(config.BlockedStates[i], stateName, StringComparison.OrdinalIgnoreCase)) return false;
            }
            return null;
        }

        private void Unload()
        {
            if (freezeTimer != null)
            {
                freezeTimer.Destroy();
                freezeTimer = null;
            }
            freezeUntil = 0f;
        }
    }
}

The plugin adds a chat command, npcstatefreeze <seconds>, that starts a server-wide window during which NPC brains may not enter Chase or Attack. OnAIBrainStateSwitch checks whether the window is active and whether newState matches one of the configured blocked states (case-insensitive); if so it returns false and the NPC stays in its current state until the window ends. A freezeUntil timestamp keeps the hook body cheap, and a single timer clears it and broadcasts to chat when the window ends. The requested duration is capped by the Maximum freeze duration in seconds config value (default 600). Access is gated by the permission npcstatefreeze.use, which is registered on plugin load and checked in the command; players without it receive a no-permission reply.

Common use cases

  • PvE grace periods after a wipe

    Block Chase and Attack transitions for a configurable window after server start so players can build without scientists or animals engaging them immediately.

  • Event arenas with pacified NPCs

    While an event is running, prevent event-area NPCs from switching into aggressive states, then let them resume normal behavior when the event ends.

  • Transition auditing for custom AI tuning

    Count or track specific state changes (for example how often brains enter Flee) over a fixed interval and report the totals to admins, without printing inside the hook itself.

Pitfalls

  • It fires for every brain on the server

    Each NPC state change goes through SwitchToState, so this hook can run hundreds of times per second on populated servers. Keep the body to a field read and a comparison; do heavy work in timers.

  • Blocking states can strand NPCs

    If you cancel Chase and Attack permanently, NPCs will keep their current state and may stand still or fail to pursue intruders. Always pair a block with a timer that restores normal behavior.

  • Returning true also cancels the switch

    Any non-null return stops the original code path, so return true blocks the transition just like return false. Return null when you want the game to proceed.

Build a plugin with OnAIBrainStateSwitch

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

Prompt: PvE grace periods after a wipe using the OnAIBrainStateSwitch 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 OnAIBrainStateSwitch

Generate