/*************************************************************************
 * ModernUO                                                              *
 * Copyright 2019-2024 - ModernUO Development Team                       *
 * Email: hi@modernuo.com                                                *
 * File: Mobile.cs                                                       *
 *                                                                       *
 * This program is free software: you can redistribute it and/or modify  *
 * it under the terms of the GNU General Public License as published by  *
 * the Free Software Foundation, either version 3 of the License, or     *
 * (at your option) any later version.                                   *
 *                                                                       *
 * You should have received a copy of the GNU General Public License     *
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. *
 *************************************************************************/

using Server.Accounting;
using Server.Collections;
using Server.ContextMenus;
using Server.Guilds;
using Server.HuePickers;
using Server.Items;
using Server.Logging;
using Server.Menus;
using Server.Mobiles;
using Server.Network;
using Server.Prompts;
using Server.Targeting;
using Server.Text;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using CalcMoves = Server.Movement.Movement;

namespace Server;

public delegate void TargetCallback(Mobile from, object targeted);

public delegate void TargetStateCallback<in T>(Mobile from, object targeted, T state);

public delegate void PromptCallback(Mobile from, string text);

public delegate void PromptStateCallback<in T>(Mobile from, string text, T state);

public class DamageEntry
{
    public DamageEntry(Mobile damager) => Damager = damager;

    public Mobile Damager { get; }

    public int DamageGiven { get; set; }

    public DateTime LastDamage { get; set; }

    public bool HasExpired => Core.Now > LastDamage + ExpireDelay;

    public List<DamageEntry> Responsible { get; set; }

    public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0);
}

[Flags]
public enum StatType
{
    Str = 1,
    Dex = 2,
    Int = 4,
    All = 7
}

public enum StatLockType : byte
{
    Up,
    Down,
    Locked
}

[Flags]
[CustomEnum(new[] { "North", "Right", "East", "Down", "South", "Left", "West", "Up" })]
public enum Direction : byte
{
    North = 0x0,
    Right = 0x1,
    East = 0x2,
    Down = 0x3,
    South = 0x4,
    Left = 0x5,
    West = 0x6,
    Up = 0x7,

    Mask = 0x7,
    Running = 0x80,
    ValueMask = 0x87
}

[Flags]
public enum MobileDelta
{
    None = 0x00000000,
    Name = 0x00000001,
    Flags = 0x00000002,
    Hits = 0x00000004,
    Mana = 0x00000008,
    Stam = 0x00000010,
    Stat = 0x00000020,
    Noto = 0x00000040,
    Gold = 0x00000080,
    Weight = 0x00000100,
    Direction = 0x00000200,
    Hue = 0x00000400,
    Body = 0x00000800,
    Armor = 0x00001000,
    StatCap = 0x00002000,
    GhostUpdate = 0x00004000,
    Followers = 0x00008000,
    Properties = 0x00010000,
    TithingPoints = 0x00020000,
    Resistances = 0x00040000,
    WeaponDamage = 0x00080000,
    Hair = 0x00100000,
    FacialHair = 0x00200000,
    Race = 0x00400000,
    HealthbarYellow = 0x00800000,
    HealthbarPoison = 0x01000000,

    Attributes = 0x0000001C
}

public enum Healthbar
{
    Normal,
    Poison,
    Yellow
}

public enum AccessLevel
{
    Player,
    Counselor,
    GameMaster,
    Seer,
    Administrator,
    Developer,
    Owner
}

public enum VisibleDamageType
{
    None,
    Related,
    Everyone,
    Selective
}

public enum ResistanceType
{
    Physical,
    Fire,
    Cold,
    Poison,
    Energy
}

public enum ApplyPoisonResult
{
    Poisoned,
    Immune,
    HigherPoisonActive,
    Cured
}

public delegate bool SkillCheckTargetHandler(
    Mobile from, SkillName skill, object target, double minSkill,
    double maxSkill
);

public delegate bool SkillCheckLocationHandler(Mobile from, SkillName skill, double minSkill, double maxSkill);

public delegate bool SkillCheckDirectTargetHandler(Mobile from, SkillName skill, object target, double chance);

public delegate bool SkillCheckDirectLocationHandler(Mobile from, SkillName skill, double chance);

public delegate TimeSpan RegenRateHandler(Mobile from);

public delegate bool AllowBeneficialHandler(Mobile from, Mobile target);

public delegate bool AllowHarmfulHandler(Mobile from, Mobile target);

public delegate Container CreateCorpseHandler(
    Mobile from, List<Item> initialContent, List<Item> equippedItems
);

public delegate int AOSStatusHandler(Mobile from, int index);

/// <summary>
///     Base class representing players, npcs, and creatures.
/// </summary>
public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Mobile>
{
    // Allow four warmode changes in 0.5 seconds, any more will be delay for two seconds
    private const int WarmodeCatchCount = 4;

    private static readonly ILogger logger = LogFactory.GetLogger(typeof(Mobile));

    // TODO: Make these configurations
    private static readonly TimeSpan WarmodeSpamCatch = TimeSpan.FromSeconds(Core.SE ? 1.0 : 0.5);
    private static readonly TimeSpan WarmodeSpamDelay = TimeSpan.FromSeconds(Core.SE ? 4.0 : 2.0);
    private static readonly TimeSpan ExpireCombatantDelay = TimeSpan.FromMinutes(1.0);
    private static readonly TimeSpan ExpireAggressorsDelay = TimeSpan.FromSeconds(5.0);

    private static readonly object m_GhostMutateContext = new();

    private static readonly List<Mobile> m_Hears = new();
    private static readonly List<IEntity> m_OnSpeech = new();

    private static readonly string[] m_AccessLevelNames =
    {
        "a player",
        "a counselor",
        "a game master",
        "a seer",
        "an administrator",
        "a developer",
        "an owner"
    };

    private static readonly int[] m_InvalidBodies =
    {
        32,
        95,
        156,
        197,
        198
    };

    private static readonly Queue<Mobile> m_DeltaQueue = new();

    private static readonly string[] m_GuildTypes =
    {
        "",
        " (Chaos)",
        " (Order)"
    };

    private static bool _disableCastParalyze = true;

    public static void Configure()
    {
        _disableCastParalyze = ServerConfiguration.GetSetting("spellCasting.disableCastParalyze", true);
    }

    private List<object> _actions;
    private AccessLevel m_AccessLevel;

    private TimerExecutionToken _autoManifestTimerToken;

    private Container m_Backpack;

    private BankBox m_BankBox;
    private Body m_Body;
    private Body m_BodyMod;
    private bool m_CanHearGhosts;

    private int m_ChangingCombatant;
    private Mobile m_Combatant;
    private TimerExecutionToken _combatTimerToken;
    private bool m_Criminal;

    private MobileDelta m_DeltaFlags;
    private Direction m_Direction;
    private bool m_DisplayGuildTitle;

    private TimerExecutionToken _expireAggrTimerToken;
    private TimerExecutionToken _expireCombatantTimerToken;
    private TimerExecutionToken _expireCriminalTimerToken;
    private VirtualHairInfo _facialHair;
    public VirtualHairInfo FacialHair => _facialHair ??= new VirtualHairInfo(FacialHairItemID, FacialHairHue);

    private int m_Fame, m_Karma;
    private bool m_Female, m_Warmode, m_Hidden, m_Blessed, m_Flying;
    private int m_Followers, m_FollowersMax;
    private bool m_Frozen;
    private TimerExecutionToken _frozenTimerToken;
    private BaseGuild m_Guild;
    private string m_GuildTitle;

    private VirtualHairInfo _hair;
    public VirtualHairInfo Hair => _hair ??= new VirtualHairInfo(HairItemID, HairHue);

    private int m_Hits, m_Stam, m_Mana;

    private Item m_Holding;
    private int m_Hue;

    private int m_HueMod = -1;
    private int m_Hunger;

    private bool m_InDeltaQueue;
    private int m_Kills;
    private string m_Language;
    private int m_LightLevel;
    private Point3D m_Location;
    private TimerExecutionToken _logoutTimerToken;
    private Timer m_ManaTimer, m_HitsTimer, m_StamTimer;

    private Map m_Map;

    /* Logout:
     *
     * When a client logs into mobile x
     *  - if (x is Internalized ) move x to logout location and map
     *
     * When a client attached to a mobile disconnects
     *  - LogoutTimer is started
     *     - Delay is taken from Region.GetLogoutDelay to allow insta-logout regions.
     *     - OnTick : Location and map are stored, and mobile is internalized
     *
     * Some things to consider:
     *  - An internalized person getting killed (say, by poison). Where does the body go?
     *  - Regions now have a GetLogoutDelay( Mobile m ); virtual function (see above)
     */

    private Item m_MountItem;
    private string m_Name;

    private string m_NameMod;
    private NetState m_NetState;
    private DateTime m_NextWarmodeChange;
    private bool m_Paralyzed;
    private TimerExecutionToken _paraTimerToken;
    private bool m_Player;
    private Poison m_Poison;
    private Prompt m_Prompt;
    private ObjectPropertyList m_PropertyList;
    private Race m_Race;
    private Region m_Region;

    private int m_SolidHueOverride = -1;
    private ISpell m_Spell;
    private int m_StatCap;
    private int m_Str, m_Dex, m_Int;

    private StatLockType m_StrLock, m_DexLock, m_IntLock;
    private Target m_Target;
    private int m_TithingPoints;
    private string m_Title;
    private int m_TotalGold, m_TotalItems, m_TotalWeight;
    private int m_VirtualArmor;
    private int m_VirtualArmorMod;
    private int m_WarmodeChanges;
    private bool _warmodeSpamValue;
    private IWeapon m_Weapon;
    private bool _ignoreMobiles;

    private bool m_YellowHealthbar;
    private List<StatMod> _statMods;
    private List<ResistanceMod> _resistanceMods;
    private List<SkillMod> _skillMods;

    public Mobile()
    {
        m_Region = Map.Internal.DefaultRegion;
        Serial = World.NewMobile;

        DefaultMobileInit();

        World.AddEntity(this);
    }

    public Mobile(Serial serial)
    {
        m_Region = Map.Internal.DefaultRegion;
        Serial = serial;
        Aggressors = new List<AggressorInfo>();
        Aggressed = new List<AggressorInfo>();
        NextSkillTime = Core.TickCount;
        DamageEntries = new List<DamageEntry>();
    }

    // Sectors
    public Mobile Next { get; set; }
    public Mobile Previous { get; set; }
    public bool OnLinkList { get; set; }

    public static bool DragEffects { get; set; } = true;

    [CommandProperty(AccessLevel.GameMaster)]
    public Race Race
    {
        get => m_Race ??= Race.DefaultRace;
        set
        {
            var oldRace = Race;

            m_Race = value ?? Race.DefaultRace;

            Body = m_Race.Body(this);
            UpdateResistances();

            Delta(MobileDelta.Race);

            OnRaceChange(oldRace);
        }
    }

    public virtual double RacialSkillBonus => 0;

    public int[] Resistances { get; private set; }

    public virtual int BasePhysicalResistance => 0;
    public virtual int BaseFireResistance => 0;
    public virtual int BaseColdResistance => 0;
    public virtual int BasePoisonResistance => 0;
    public virtual int BaseEnergyResistance => 0;

    [CommandProperty(AccessLevel.Counselor)]
    public virtual int PhysicalResistance => GetResistance(ResistanceType.Physical);

    [CommandProperty(AccessLevel.Counselor)]
    public virtual int FireResistance => GetResistance(ResistanceType.Fire);

    [CommandProperty(AccessLevel.Counselor)]
    public virtual int ColdResistance => GetResistance(ResistanceType.Cold);

    [CommandProperty(AccessLevel.Counselor)]
    public virtual int PoisonResistance => GetResistance(ResistanceType.Poison);

    [CommandProperty(AccessLevel.Counselor)]
    public virtual int EnergyResistance => GetResistance(ResistanceType.Energy);

    public static int MaxPlayerResistance { get; set; } = 70;

    public virtual bool NewGuildDisplay => false;

    [CommandProperty(AccessLevel.GameMaster)]
    public virtual bool IgnoreMobiles
    {
        get => _ignoreMobiles;
        set
        {
            if (_ignoreMobiles != value)
            {
                _ignoreMobiles = value;
                Delta(MobileDelta.Flags);
            }
        }
    }

    public object Party { get; set; }

    public List<SkillMod> SkillMods => _skillMods;
    public List<StatMod> StatMods => _statMods;
    public List<ResistanceMod> ResistanceMods => _resistanceMods;

    [CommandProperty(AccessLevel.GameMaster)]
    public int VirtualArmorMod
    {
        get => m_VirtualArmorMod;
        set
        {
            if (m_VirtualArmorMod != value)
            {
                m_VirtualArmorMod = value;

                Delta(MobileDelta.Armor);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int MeleeDamageAbsorb { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int MagicDamageAbsorb { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int SkillsTotal => Skills?.Total ?? 0;

    [CommandProperty(AccessLevel.GameMaster)]
    public int SkillsCap
    {
        get => Skills?.Cap ?? 0;
        set
        {
            if (Skills != null)
            {
                Skills.Cap = value;
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int BaseSoundID { get; set; }

    public long NextCombatTime { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int NameHue { get; set; } = -1;

    [CommandProperty(AccessLevel.GameMaster)]
    public int Hunger
    {
        get => m_Hunger;
        set
        {
            var oldValue = m_Hunger;

            if (oldValue != value)
            {
                m_Hunger = value;
                OnHungerChanged(oldValue);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int Thirst { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int BAC { get; set; }

    /// <summary>
    ///     Gets or sets the number of steps this player may take when hidden before being revealed.
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int AllowedStealthSteps { get; set; }

    public Item Holding
    {
        get => m_Holding;
        set
        {
            if (m_Holding != value)
            {
                if (m_Holding != null)
                {
                    UpdateTotal(m_Holding, TotalType.Weight, -(m_Holding.TotalWeight + m_Holding.PileWeight));

                    if (m_Holding.HeldBy == this)
                    {
                        m_Holding.HeldBy = null;
                    }
                }

                if (value != null && m_Holding != null)
                {
                    DropHolding();
                }

                m_Holding = value;

                if (m_Holding != null)
                {
                    UpdateTotal(m_Holding, TotalType.Weight, m_Holding.TotalWeight + m_Holding.PileWeight);

                    m_Holding.HeldBy ??= this;
                }
            }
        }
    }

    public long LastMoveTime { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public virtual bool Paralyzed
    {
        get => m_Paralyzed;
        set
        {
            if (m_Paralyzed != value)
            {
                m_Paralyzed = value;
                Delta(MobileDelta.Flags);

                SendLocalizedMessage(m_Paralyzed ? 502381 : 502382);
                _paraTimerToken.Cancel();
            }

            if (!value && m_NetState != null)
            {
                var now = Core.TickCount;
                if (now - m_NetState._nextMovementTime > 0)
                {
                    m_NetState._nextMovementTime = now;
                }
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool DisarmReady { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool StunReady { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Frozen
    {
        get => m_Frozen;
        set
        {
            if (m_Frozen != value)
            {
                m_Frozen = value;
                Delta(MobileDelta.Flags);
                _frozenTimerToken.Cancel();
            }
        }
    }

    /// <summary>
    ///     Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawStr" /> property.
    /// </summary>
    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public StatLockType StrLock
    {
        get => m_StrLock;
        set
        {
            if (m_StrLock != value)
            {
                m_StrLock = value;

                m_NetState.SendStatLockInfo(this);
            }
        }
    }

    /// <summary>
    ///     Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawDex" /> property.
    /// </summary>
    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public StatLockType DexLock
    {
        get => m_DexLock;
        set
        {
            if (m_DexLock != value)
            {
                m_DexLock = value;

                m_NetState.SendStatLockInfo(this);
            }
        }
    }

    /// <summary>
    ///     Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawInt" /> property.
    /// </summary>
    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public StatLockType IntLock
    {
        get => m_IntLock;
        set
        {
            if (m_IntLock != value)
            {
                m_IntLock = value;

                m_NetState.SendStatLockInfo(this);
            }
        }
    }

    public long NextActionTime { get; set; }

    public long NextActionMessage { get; set; }

    public static int ActionMessageDelay { get; set; } = 125;

    public static bool GlobalRegenThroughPoison { get; set; } = true;

    public virtual bool RegenThroughPoison => GlobalRegenThroughPoison;

    public virtual bool CanRegenHits => Alive && (RegenThroughPoison || !Poisoned);
    public virtual bool CanRegenStam => Alive;
    public virtual bool CanRegenMana => Alive;

    public long NextSkillTime { get; set; }

    public List<AggressorInfo> Aggressors { get; private set; }

    public List<AggressorInfo> Aggressed { get; private set; }

    public bool ChangingCombatant => m_ChangingCombatant > 0;

    private void ExpireCombatant()
    {
        Combatant = null;
        _expireCombatantTimerToken.Cancel();
    }

    /// <summary>
    ///     Overridable. Gets or sets which Mobile that this Mobile is currently engaged in combat with.
    ///     <seealso cref="OnCombatantChange" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual Mobile Combatant
    {
        get => m_Combatant;
        set
        {
            if (Deleted)
            {
                return;
            }

            if (m_Combatant != value && value != this)
            {
                var old = m_Combatant;

                ++m_ChangingCombatant;
                m_Combatant = value;

                if (m_Combatant != null && !CanBeHarmful(m_Combatant, false) ||
                    !Region.OnCombatantChange(this, old, m_Combatant))
                {
                    m_Combatant = old;
                    --m_ChangingCombatant;
                    return;
                }

                if (m_Combatant == null)
                {
                    m_NetState.SendChangeCombatant(Serial.Zero);
                    _expireCombatantTimerToken.Cancel();
                    _combatTimerToken.Cancel();
                }
                else
                {
                    m_NetState.SendChangeCombatant(m_Combatant.Serial);
                    if (!_expireCombatantTimerToken.Running)
                    {
                        Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
                    }

                    if (!_combatTimerToken.Running)
                    {
                        Timer.StartTimer(TimeSpan.FromSeconds(0.01), 0, CheckCombatTime, out _combatTimerToken);
                    }

                    if (CanBeHarmful(m_Combatant, false))
                    {
                        DoHarmful(m_Combatant); // due to reflection, might make m_Combatant null
                        m_Combatant?.PlaySound(m_Combatant.GetAngerSound());
                    }
                }

                OnCombatantChange();
                --m_ChangingCombatant;
            }
        }
    }

    private void CheckCombatTime()
    {
        if (Core.TickCount - NextCombatTime < 0)
        {
            return;
        }

        var combatant = Combatant;

        // If no combatant, wrong map, one of us is a ghost, or cannot see, or deleted, then stop combat
        if (combatant?.Deleted != false || Deleted || combatant.m_Map != m_Map ||
            !combatant.Alive || !Alive || !CanSee(combatant) || combatant.IsDeadBondedPet ||
            IsDeadBondedPet)
        {
            Combatant = null;
            return;
        }

        var weapon = Weapon;

        if (weapon == null || !InRange(combatant, weapon.MaxRange))
        {
            return;
        }

        if (InLOS(combatant))
        {
            weapon.OnBeforeSwing(this, combatant);
            RevealingAction();
            NextCombatTime =
                Core.TickCount + (int)weapon.OnSwing(this, combatant).TotalMilliseconds;
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int TotalGold => GetTotal(TotalType.Gold);

    [CommandProperty(AccessLevel.GameMaster)]
    public int TotalItems => GetTotal(TotalType.Items);

    [CommandProperty(AccessLevel.GameMaster)]
    public int TotalWeight => GetTotal(TotalType.Weight);

    [CommandProperty(AccessLevel.GameMaster)]
    public int TithingPoints
    {
        get => m_TithingPoints;
        set
        {
            if (m_TithingPoints != value)
            {
                m_TithingPoints = value;

                Delta(MobileDelta.TithingPoints);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int Followers
    {
        get => m_Followers;
        set
        {
            if (m_Followers != value)
            {
                m_Followers = value;

                Delta(MobileDelta.Followers);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int FollowersMax
    {
        get => m_FollowersMax;
        set
        {
            if (m_FollowersMax != value)
            {
                m_FollowersMax = value;

                Delta(MobileDelta.Followers);
            }
        }
    }

    public bool TargetLocked { get; set; }

    public Target Target
    {
        get => m_Target;
        set
        {
            var oldTarget = m_Target;
            var newTarget = value;

            if (oldTarget == newTarget)
            {
                return;
            }

            m_Target = null;

            if (oldTarget != null && newTarget != null)
            {
                oldTarget.Cancel(this, TargetCancelType.Overridden);
            }

            m_Target = newTarget;

            if (newTarget != null && !TargetLocked)
            {
                newTarget.SendTargetTo(m_NetState);
            }

            OnTargetChange();
        }
    }

    [IgnoreDupe]
    public bool Pushing { get; set; }

    public virtual bool IsDeadBondedPet => false;

    [IgnoreDupe]
    public ISpell Spell
    {
        get => m_Spell;
        set
        {
            if (m_Spell != null && value != null)
            {
                logger.Warning("Spell has been overwritten.");
            }

            m_Spell = value;
        }
    }

    [CommandProperty(AccessLevel.Administrator)]
    public bool AutoPageNotify { get; set; }

    [CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
    public IAccount Account { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int VirtualArmor
    {
        get => m_VirtualArmor;
        set
        {
            if (m_VirtualArmor != value)
            {
                m_VirtualArmor = value;

                Delta(MobileDelta.Armor);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public virtual double ArmorRating => 0.0;

    /// <summary>
    ///     Overridable. Returns true if the player is alive, false if otherwise. By default, this is computed by:
    ///     <c>!Deleted &amp;&amp; (!Player || !Body.IsGhost)</c>
    /// </summary>
    [CommandProperty(AccessLevel.Counselor)]
    public virtual bool Alive => !Deleted && (!m_Player || !m_Body.IsGhost);

    public static CreateCorpseHandler CreateCorpseHandler { get; set; }

    public virtual bool RetainPackLocsOnDeath => Core.AOS;

    [CommandProperty(AccessLevel.GameMaster)]
    public Container Corpse { get; set; }

    public static char[] GhostChars { get; set; }
    public static bool NoSpeechLOS { get; set; }

    public static TimeSpan AutoManifestTimeout { get; set; } = TimeSpan.FromSeconds(5.0);

    public static bool InsuranceEnabled { get; set; }

    public static int ActionDelay { get; set; } = 500;

    public static VisibleDamageType VisibleDamageType { get; set; }

    public List<DamageEntry> DamageEntries { get; private set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public Mobile LastKiller { get; set; }

    public static bool DefaultShowVisibleDamage { get; set; }

    public static bool DefaultCanSeeVisibleDamage { get; set; }

    public virtual bool ShowVisibleDamage => DefaultShowVisibleDamage;
    public virtual bool CanSeeVisibleDamage => DefaultCanSeeVisibleDamage;

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Squelched { get; set; }

    public virtual bool ShouldCheckStatTimers => true;

    [CommandProperty(AccessLevel.GameMaster)]
    public int LightLevel
    {
        get => m_LightLevel;
        set
        {
            if (m_LightLevel != value)
            {
                m_LightLevel = value;

                CheckLightLevels(false);
            }
        }
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public string Profile { get; set; }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public bool ProfileLocked { get; set; }

    [CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
    public bool Player
    {
        get => m_Player;
        set
        {
            m_Player = value;
            InvalidateProperties();
            CheckStatTimers();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public string Title
    {
        get => m_Title;
        set
        {
            m_Title = value;
            InvalidateProperties();
        }
    }

    public List<Item> Items { get; private set; }

    public virtual int MaxWeight => int.MaxValue;

    public static IWeapon DefaultWeapon { get; set; }

    [CommandProperty(AccessLevel.Counselor, canModify: true)]
    public Skills Skills { get; private set; }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.Administrator)]
    public AccessLevel AccessLevel
    {
        get => m_AccessLevel;
        set
        {
            var oldValue = m_AccessLevel;

            if (oldValue != value)
            {
                m_AccessLevel = value;
                Delta(MobileDelta.Noto);
                InvalidateProperties();

                SendMessage($"Your access level has been changed. You are now {GetAccessLevelName(value)}.");

                ClearScreen();
                SendEverything();

                OnAccessLevelChanged(oldValue);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int Fame
    {
        get => m_Fame;
        set
        {
            var oldValue = m_Fame;

            if (oldValue != value)
            {
                m_Fame = value;

                if (ShowFameTitle && (m_Player || m_Body.IsHuman) && oldValue >= 10000 != value >= 10000)
                {
                    InvalidateProperties();
                }

                OnFameChange(oldValue);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int Karma
    {
        get => m_Karma;
        set
        {
            var old = m_Karma;

            if (old != value)
            {
                m_Karma = value;
                OnKarmaChange(old);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Blessed
    {
        get => m_Blessed;
        set
        {
            if (m_Blessed != value)
            {
                m_Blessed = value;
                Delta(MobileDelta.HealthbarYellow);
            }
        }
    }

    public virtual int Luck => 0;

    [Hue]
    [CommandProperty(AccessLevel.GameMaster)]
    public int HueMod
    {
        get => m_HueMod;
        set
        {
            if (m_HueMod != value)
            {
                m_HueMod = value;

                Delta(MobileDelta.Hue);
            }
        }
    }

    [Hue]
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int Hue
    {
        get => m_HueMod != -1 ? m_HueMod : m_Hue;
        set
        {
            var oldHue = m_Hue;

            if (oldHue != value)
            {
                m_Hue = value;

                Delta(MobileDelta.Hue);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public Direction Direction
    {
        get => m_Direction;
        set
        {
            if (m_Direction != value)
            {
                m_Direction = value;
                Delta(MobileDelta.Direction);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Female
    {
        get => m_Female;
        set
        {
            if (m_Female != value)
            {
                m_Female = value;
                Delta(MobileDelta.Flags);
                OnGenderChanged(!m_Female);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Flying
    {
        get => m_Flying;
        set
        {
            if (m_Flying != value)
            {
                m_Flying = value;
                Delta(MobileDelta.Flags);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Warmode
    {
        get => m_Warmode;
        set
        {
            if (Deleted)
            {
                return;
            }

            if (m_Warmode != value)
            {
                _autoManifestTimerToken.Cancel();

                m_Warmode = value;
                Delta(MobileDelta.Flags);

                m_NetState.SendSetWarMode(value);

                if (!m_Warmode)
                {
                    Combatant = null;
                }

                if (!Alive)
                {
                    if (value)
                    {
                        Delta(MobileDelta.GhostUpdate);
                    }
                    else
                    {
                        SendRemovePacket(false);
                    }
                }

                OnWarmodeChanged();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Hidden
    {
        get => m_Hidden;
        set
        {
            if (m_Hidden != value)
            {
                m_Hidden = value;
                // Delta( MobileDelta.Flags );

                OnHiddenChanged();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
    public NetState NetState
    {
        get
        {
            if (m_NetState?.Connection == null)
            {
                m_NetState = null;
            }

            return m_NetState;
        }
        set
        {
            if (m_NetState == value)
            {
                return;
            }

#if THREADGUARD
            if (Thread.CurrentThread != Core.Thread)
            {
                Utility.PushColor(ConsoleColor.Red);
                Console.WriteLine("Attempting to set Mobile.NetState value from an invalid thread!");
                Console.WriteLine(new StackTrace());
                Utility.PopColor();
                return;
            }
#endif

            m_Map?.OnClientChange(m_NetState, value, this);
            m_Target?.Cancel(this, TargetCancelType.Disconnected);
            m_Spell?.OnConnectionChanged();
            m_NetState?.CancelAllTrades();

            var box = FindBankNoCreate();

            if (box?.Opened == true)
            {
                box.Close();
            }

            OnBeforeDisconnected();
            EventSink.InvokeBeforeDisconnected(this);

            m_NetState = value;
            _logoutTimerToken.Cancel();

            if (m_NetState == null)
            {
                OnDisconnected();
                EventSink.InvokeDisconnected(this);

                // Disconnected, start the logout timer
                Timer.StartTimer(GetLogoutDelay(), Logout, out _logoutTimerToken);
            }
            else
            {
                OnConnected();
                EventSink.InvokeConnected(this);

                if (m_Map == Map.Internal && LogoutMap != null)
                {
                    Map = LogoutMap;
                    Location = LogoutLocation;
                }
            }

            for (var i = Items.Count - 1; i >= 0; --i)
            {
                if (i >= Items.Count)
                {
                    continue;
                }

                var item = Items[i];

                if (item is SecureTradeContainer)
                {
                    for (var j = item.Items.Count - 1; j >= 0; --j)
                    {
                        if (j < item.Items.Count)
                        {
                            item.Items[j].OnSecureTrade(this, this, this, false);
                            AddToBackpack(item.Items[j]);
                        }
                    }

                    Timer.StartTimer(item.Delete);
                }
            }

            DropHolding();
            OnNetStateChanged();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public string Language
    {
        get => m_Language;
        set => m_Language = value;
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int SpeechHue { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int EmoteHue { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int WhisperHue { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public int YellHue { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public string GuildTitle
    {
        get => m_GuildTitle;
        set
        {
            var old = m_GuildTitle;

            if (old != value)
            {
                m_GuildTitle = value;

                if (m_Guild?.Disbanded == false && m_GuildTitle != null)
                {
                    SendLocalizedMessage(1018026, true, m_GuildTitle); // Your guild title has changed :
                }

                InvalidateProperties();

                OnGuildTitleChange(old);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool DisplayGuildTitle
    {
        get => m_DisplayGuildTitle;
        set
        {
            m_DisplayGuildTitle = value;
            InvalidateProperties();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public Mobile GuildFealty { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public string NameMod
    {
        get => m_NameMod;
        set
        {
            if (m_NameMod != value)
            {
                m_NameMod = value;
                Delta(MobileDelta.Name);
                InvalidateProperties();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool YellowHealthbar
    {
        get => m_YellowHealthbar;
        set
        {
            m_YellowHealthbar = value;
            Delta(MobileDelta.HealthbarYellow);
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public string RawName
    {
        get => m_Name;
        set => Name = value;
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public virtual string Name
    {
        get => m_NameMod ?? m_Name;
        set
        {
            if (m_Name != value) // I'm leaving out the && m_NameMod == null
            {
                var oldName = m_Name;
                m_Name = value;
                OnAfterNameChange(oldName, m_Name);
                Delta(MobileDelta.Name);
                InvalidateProperties();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public DateTime LastStrGain { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public DateTime LastIntGain { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public DateTime LastDexGain { get; set; }

    public DateTime LastStatGain
    {
        get
        {
            var d = LastStrGain;

            if (LastIntGain > d)
            {
                d = LastIntGain;
            }

            if (LastDexGain > d)
            {
                d = LastDexGain;
            }

            return d;
        }
        set
        {
            LastStrGain = value;
            LastIntGain = value;
            LastDexGain = value;
        }
    }

    public BaseGuild Guild
    {
        get => m_Guild;
        set
        {
            var old = m_Guild;

            if (old != value)
            {
                if (value == null)
                {
                    GuildTitle = null;
                }

                m_Guild = value;

                Delta(MobileDelta.Noto);
                InvalidateProperties();

                OnGuildChange(old);
            }
        }
    }

    public Region WalkRegion { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Poisoned => m_Poison != null;

    [CommandProperty(AccessLevel.GameMaster)]
    public bool IsBodyMod => m_BodyMod.BodyID != 0;

    [CommandProperty(AccessLevel.GameMaster)]
    public Body BodyMod
    {
        get => m_BodyMod;
        set
        {
            if (m_BodyMod != value)
            {
                m_BodyMod = value;

                Delta(MobileDelta.Body);
                InvalidateProperties();

                CheckStatTimers();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public Body Body
    {
        get => IsBodyMod ? m_BodyMod : m_Body;
        set
        {
            if (m_Body != value && !IsBodyMod)
            {
                m_Body = SafeBody(value);

                Delta(MobileDelta.Body);
                InvalidateProperties();

                CheckStatTimers();
            }
        }
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public Point3D LogoutLocation { get; set; }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public Map LogoutMap { get; set; }

    public Region Region => m_Region ?? (Map == null ? Map.Internal.DefaultRegion : Map.DefaultRegion);

    [CommandProperty(AccessLevel.GameMaster)]
    public int SolidHueOverride
    {
        get => m_SolidHueOverride;
        set
        {
            if (m_SolidHueOverride == value)
            {
                return;
            }

            m_SolidHueOverride = value;
            Delta(MobileDelta.Hue | MobileDelta.Body);
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public virtual IWeapon Weapon
    {
        get
        {
            if (m_Weapon is Item item && !item.Deleted && item.Parent == this && CanSee(item))
            {
                return m_Weapon;
            }

            m_Weapon = null;

            item = FindItemOnLayer(Layer.OneHanded) ?? FindItemOnLayer(Layer.TwoHanded);

            if (item is IWeapon weapon)
            {
                return m_Weapon = weapon;
            }

            return GetDefaultWeapon();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public BankBox BankBox
    {
        get
        {
            if (m_BankBox?.Deleted == false && m_BankBox.Parent == this)
            {
                return m_BankBox;
            }

            m_BankBox = FindItemOnLayer<BankBox>(Layer.Bank);

            if (m_BankBox == null)
            {
                AddItem(m_BankBox = new BankBox(this));
            }

            return m_BankBox;
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public Container Backpack
    {
        get
        {
            if (m_Backpack?.Deleted != false || m_Backpack.Parent != this)
            {
                m_Backpack = FindItemOnLayer<Container>(Layer.Backpack);
            }

            return m_Backpack;
        }
    }

    public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player;

    public bool HasTrade => m_NetState?.Trades.Count > 0;

    public bool NoMoveHS { get; set; }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public int Kills
    {
        get => m_Kills;
        set
        {
            var oldValue = m_Kills;

            if (m_Kills != value)
            {
                m_Kills = Math.Max(value, 0);

                if (oldValue >= 5 != m_Kills >= 5)
                {
                    Delta(MobileDelta.Noto);
                    InvalidateProperties();
                }

                OnKillsChange(oldValue);
            }
        }
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public virtual bool Criminal
    {
        get => m_Criminal;
        set
        {
            if (m_Criminal != value)
            {
                m_Criminal = value;
                Delta(MobileDelta.Noto);
                InvalidateProperties();
            }

            _expireCriminalTimerToken.Cancel();

            if (m_Criminal)
            {
                Timer.StartTimer(ExpireCriminalDelay, ExpireCriminal, out _expireCriminalTimerToken);
            }
        }
    }

    public static bool DisableDismountInWarmode { get; set; }

    public static int BodyWeight { get; set; } = 11; // 11 + 3 for the backpack

    [CommandProperty(AccessLevel.GameMaster)]
    public IMount Mount
    {
        get
        {
            Item item = null;

            if (m_MountItem?.Deleted == false && m_MountItem.Parent == this)
            {
                item = m_MountItem;
            }

            item ??= FindItemOnLayer(Layer.Mount);

            if (item is not IMountItem mountItem)
            {
                return null;
            }

            m_MountItem = item;
            return mountItem.Mount;
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Mounted => Mount != null;

    public virtual bool CanTarget => true;
    public virtual bool ClickTitle => true;

    public virtual bool PropertyTitle => OldPropertyTitles ? ClickTitle : true;

    public static bool DisableHiddenSelfClick { get; set; } = true;

    public static bool AsciiClickMessage { get; set; } = true;

    public static bool GuildClickMessage { get; set; } = true;

    public static bool OldPropertyTitles { get; set; }

    public virtual bool ShowFameTitle // (m_Player || m_Body.IsHuman) && m_Fame >= 10000; }
        => true;

    /// <summary>
    ///     Gets or sets the maximum attainable value for <see cref="RawStr" />, <see cref="RawDex" />, and <see cref="RawInt" />.
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int StatCap
    {
        get => m_StatCap;
        set
        {
            if (m_StatCap != value)
            {
                m_StatCap = value;

                Delta(MobileDelta.StatCap);
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool Meditating { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool CanSwim { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool CantWalk { get; set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public bool CanHearGhosts
    {
        get => m_CanHearGhosts || AccessLevel >= AccessLevel.Counselor;
        set => m_CanHearGhosts = value;
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int RawStatTotal => RawStr + RawDex + RawInt;

    public long NextSpellTime { get; set; }

    public static AllowBeneficialHandler AllowBeneficialHandler { get; set; }

    public static AllowHarmfulHandler AllowHarmfulHandler { get; set; }

    public static SkillCheckTargetHandler SkillCheckTargetHandler { get; set; }

    public static SkillCheckLocationHandler SkillCheckLocationHandler { get; set; }

    public static SkillCheckDirectTargetHandler SkillCheckDirectTargetHandler { get; set; }

    public static SkillCheckDirectLocationHandler SkillCheckDirectLocationHandler { get; set; }

    public static AOSStatusHandler AOSStatusHandler { get; set; }

    public static RegenRateHandler HitsRegenRateHandler { get; set; }

    public static TimeSpan DefaultHitsRate { get; set; }

    public static RegenRateHandler StamRegenRateHandler { get; set; }

    public static TimeSpan DefaultStamRate { get; set; }

    public static RegenRateHandler ManaRegenRateHandler { get; set; }

    public static TimeSpan DefaultManaRate { get; set; }

    public static TimeSpan ExpireCriminalDelay { get; set; } = TimeSpan.FromMinutes(2.0);

    public Prompt Prompt
    {
        get => m_Prompt;
        set
        {
            var oldPrompt = m_Prompt;
            var newPrompt = value;

            if (oldPrompt == newPrompt)
            {
                return;
            }

            m_Prompt = null;

            // TODO: Cancel the prompt anyway?
            if (newPrompt != null)
            {
                oldPrompt?.OnCancel(this);
            }

            m_Prompt = newPrompt;
            NetState.SendPrompt(newPrompt);
        }
    }

    /// <summary>
    ///     Gets or sets the base, unmodified, strength of the Mobile. Ranges from 1 to 65000, inclusive.
    ///     <seealso cref="Str" />
    ///     <seealso cref="StatMod" />
    ///     <seealso cref="OnRawStrChange" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int RawStr
    {
        get => m_Str;
        set
        {
            value = Math.Clamp(value, 1, 65000);

            if (m_Str != value)
            {
                var oldValue = m_Str;

                m_Str = value;
                Delta(MobileDelta.Stat | MobileDelta.Hits);

                if (Hits < HitsMax)
                {
                    m_HitsTimer ??= new HitsTimer(this);
                    m_HitsTimer.Start();
                }
                else if (Hits > HitsMax)
                {
                    Hits = HitsMax;
                }

                OnRawStrChange(oldValue);
                OnRawStatChange(StatType.Str, oldValue);
            }
        }
    }

    /// <summary>
    ///     Gets or sets the effective strength of the Mobile. This is the sum of the <see cref="RawStr" /> plus any additional
    ///     modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
    ///     It ranges from 1 to 65000, inclusive.
    ///     <seealso cref="RawStr" />
    ///     <seealso cref="StatMod" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int Str
    {
        get => Math.Clamp(m_Str + GetStatOffset(StatType.Str), 1, 65000);
        set
        {
            if (_statMods == null || _statMods.Count == 0)
            {
                RawStr = value;
            }
        }
    }

    /// <summary>
    ///     Gets or sets the base, unmodified, dexterity of the Mobile. Ranges from 1 to 65000, inclusive.
    ///     <seealso cref="Dex" />
    ///     <seealso cref="StatMod" />
    ///     <seealso cref="OnRawDexChange" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int RawDex
    {
        get => m_Dex;
        set
        {
            value = Math.Clamp(value, 1, 65000);

            if (m_Dex != value)
            {
                var oldValue = m_Dex;

                m_Dex = value;
                Delta(MobileDelta.Stat | MobileDelta.Stam);

                if (Stam < StamMax)
                {
                    m_StamTimer ??= new StamTimer(this);
                    m_StamTimer.Start();
                }
                else if (Stam > StamMax)
                {
                    Stam = StamMax;
                }

                OnRawDexChange(oldValue);
                OnRawStatChange(StatType.Dex, oldValue);
            }
        }
    }

    /// <summary>
    ///     Gets or sets the effective dexterity of the Mobile. This is the sum of the <see cref="RawDex" /> plus any additional
    ///     modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
    ///     It ranges from 1 to 65000, inclusive.
    ///     <seealso cref="RawDex" />
    ///     <seealso cref="StatMod" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int Dex
    {
        get => Math.Clamp(m_Dex + GetStatOffset(StatType.Dex), 0, 65000);
        set
        {
            if (_statMods == null || _statMods.Count == 0)
            {
                RawDex = value;
            }
        }
    }

    /// <summary>
    ///     Gets or sets the base, unmodified, intelligence of the Mobile. Ranges from 1 to 65000, inclusive.
    ///     <seealso cref="Int" />
    ///     <seealso cref="StatMod" />
    ///     <seealso cref="OnRawIntChange" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int RawInt
    {
        get => m_Int;
        set
        {
            value = Math.Clamp(value, 1, 65000);

            if (m_Int != value)
            {
                var oldValue = m_Int;

                m_Int = value;
                Delta(MobileDelta.Stat | MobileDelta.Mana);

                if (Mana < ManaMax)
                {
                    m_ManaTimer ??= new ManaTimer(this);
                    m_ManaTimer.Start();
                }
                else if (Mana > ManaMax)
                {
                    Mana = ManaMax;
                }

                OnRawIntChange(oldValue);
                OnRawStatChange(StatType.Int, oldValue);
            }
        }
    }

    /// <summary>
    ///     Gets or sets the effective intelligence of the Mobile. This is the sum of the <see cref="RawInt" /> plus any additional
    ///     modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
    ///     It ranges from 1 to 65000, inclusive.
    ///     <seealso cref="RawInt" />
    ///     <seealso cref="StatMod" />
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int Int
    {
        get => Math.Clamp(m_Int + GetStatOffset(StatType.Int), 0, 65000);
        set
        {
            if (_statMods == null || _statMods.Count == 0)
            {
                RawInt = value;
            }
        }
    }

    /// <summary>
    ///     Gets or sets the current hit point of the Mobile. This value ranges from 0 to <see cref="HitsMax" />, inclusive. When
    ///     set
    ///     to the value of <see cref="HitsMax" />, the <see cref="AggressorInfo.CanReportMurder">CanReportMurder</see> flag of all
    ///     aggressors is reset to false, and the list of damage entries is cleared.
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int Hits
    {
        get => m_Hits;
        set
        {
            if (Deleted)
            {
                return;
            }

            value = Math.Clamp(value, 0, HitsMax);

            if (value == HitsMax)
            {
                m_HitsTimer?.Stop();

                for (var i = 0; i < Aggressors.Count; i++) // reset reports on full HP
                {
                    Aggressors[i].CanReportMurder = false;
                }

                if (DamageEntries.Count > 0)
                {
                    DamageEntries.Clear(); // reset damage entries on full HP
                }
            }
            else if (CanRegenHits)
            {
                m_HitsTimer ??= new HitsTimer(this);
                m_HitsTimer.Start();
            }
            else
            {
                m_HitsTimer?.Stop();
            }

            if (m_Hits != value)
            {
                var oldValue = m_Hits;
                m_Hits = value;
                Delta(MobileDelta.Hits);
                OnHitsChange(oldValue);
            }
        }
    }

    /// <summary>
    ///     Overridable. Gets the maximum hit point of the Mobile. By default, this returns: <c>50 + (<see cref="Str" /> / 2)</c>
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int HitsMax => 50 + Str / 2;

    /// <summary>
    ///     Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="StamMax" />, inclusive.
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int Stam
    {
        get => m_Stam;
        set
        {
            if (Deleted)
            {
                return;
            }

            value = Math.Clamp(value, 0, StamMax);

            if (CanRegenStam && value < StamMax)
            {
                m_StamTimer ??= new StamTimer(this);
                m_StamTimer.Start();
            }
            else
            {
                m_StamTimer?.Stop();
            }

            if (m_Stam != value)
            {
                var oldValue = m_Stam;
                m_Stam = value;
                Delta(MobileDelta.Stam);
                OnStamChange(oldValue);
            }
        }
    }

    /// <summary>
    ///     Overridable. Gets the maximum stamina of the Mobile. By default, this returns:
    ///     <c>
    ///         <see cref="Dex" />
    ///     </c>
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int StamMax => Dex;

    /// <summary>
    ///     Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="ManaMax" />, inclusive.
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public int Mana
    {
        get => m_Mana;
        set
        {
            if (Deleted)
            {
                return;
            }

            value = Math.Clamp(value, 0, ManaMax);

            if (value == ManaMax)
            {
                m_ManaTimer?.Stop();

                if (Meditating)
                {
                    Meditating = false;
                    SendLocalizedMessage(501846); // You are at peace.
                }
            }
            else if (CanRegenMana)
            {
                m_ManaTimer ??= new ManaTimer(this);
                m_ManaTimer.Start();
            }
            else
            {
                m_ManaTimer?.Stop();
            }

            if (m_Mana != value)
            {
                var oldValue = m_Mana;
                m_Mana = value;
                Delta(MobileDelta.Mana);
                OnManaChange(oldValue);
            }
        }
    }

    /// <summary>
    ///     Overridable. Gets the maximum mana of the Mobile. By default, this returns:
    ///     <c>
    ///         <see cref="Int" />
    ///     </c>
    /// </summary>
    [CommandProperty(AccessLevel.GameMaster)]
    public virtual int ManaMax => Int;

    public Timer PoisonTimer { get; private set; }

    [CommandProperty(AccessLevel.GameMaster)]
    public Poison Poison
    {
        get => m_Poison;
        set
        {
            m_Poison = value;
            Delta(MobileDelta.HealthbarPoison);

            if (PoisonTimer != null)
            {
                PoisonTimer.Stop();
                PoisonTimer = null;
            }

            if (m_Poison != null)
            {
                PoisonTimer = m_Poison.ConstructTimer(this);

                PoisonTimer?.Start();
            }

            CheckStatTimers();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int HairItemID
    {
        get => _hair?.ItemId ?? 0;
        set
        {
            if (_hair == null && value > 0)
            {
                _hair = new VirtualHairInfo(value);
            }
            else if (value <= 0)
            {
                _hair = null;
            }
            else if (_hair != null)
            {
                _hair.ItemId = value;
            }

            Delta(MobileDelta.Hair);
            this.MarkDirty();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int FacialHairItemID
    {
        get => _facialHair?.ItemId ?? 0;
        set
        {
            if (_facialHair == null && value > 0)
            {
                _facialHair = new VirtualHairInfo(value);
            }
            else if (value <= 0)
            {
                _facialHair = null;
            }
            else if (_facialHair != null)
            {
                _facialHair.ItemId = value;
            }

            Delta(MobileDelta.FacialHair);
            this.MarkDirty();
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int HairHue
    {
        get => _hair?.Hue ?? 0;
        set
        {
            if (_hair != null)
            {
                _hair.Hue = value;
                Delta(MobileDelta.Hair);
                this.MarkDirty();
            }
        }
    }

    [CommandProperty(AccessLevel.GameMaster)]
    public int FacialHairHue
    {
        get => _facialHair?.Hue ?? 0;
        set
        {
            if (_facialHair != null)
            {
                _facialHair.Hue = value;
                Delta(MobileDelta.FacialHair);
                this.MarkDirty();
            }
        }
    }

    public Item ShieldArmor => FindItemOnLayer(Layer.TwoHanded);

    public Item NeckArmor => FindItemOnLayer(Layer.Neck);

    public Item HandArmor => FindItemOnLayer(Layer.Gloves);

    public Item HeadArmor => FindItemOnLayer(Layer.Helm);

    public Item ArmsArmor => FindItemOnLayer(Layer.Arms);

    public Item LegsArmor => FindItemOnLayer(Layer.InnerLegs) ?? FindItemOnLayer(Layer.Pants);

    public Item ChestArmor => FindItemOnLayer(Layer.InnerTorso) ?? FindItemOnLayer(Layer.Shirt);

    public Item Talisman => FindItemOnLayer(Layer.Talisman);

    public int CompareTo(Mobile other) => other == null ? -1 : Serial.CompareTo(other.Serial);

    public virtual int HuedItemID => m_Female ? 0x2107 : 0x2106;
    public ObjectPropertyList PropertyList => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this));

    public virtual void GetProperties(IPropertyList list)
    {
        AddNameProperties(list);
    }

    [IgnoreDupe]
    [CommandProperty(AccessLevel.GameMaster, readOnly: true)]
    public DateTime Created { get; set; } = Core.Now;

    [IgnoreDupe]
    [CommandProperty(AccessLevel.Counselor)]
    public Serial Serial { get; }

    public virtual void Serialize(IGenericWriter writer)
    {
        writer.Write(36); // version

        writer.WriteDeltaTime(LastStrGain);
        writer.WriteDeltaTime(LastIntGain);
        writer.WriteDeltaTime(LastDexGain);

        byte hairflag = 0x00;

        if (_hair != null)
        {
            hairflag |= 0x01;
        }

        if (_facialHair != null)
        {
            hairflag |= 0x02;
        }

        writer.Write(hairflag);

        if ((hairflag & 0x01) != 0)
        {
            _hair?.Serialize(writer);
        }

        if ((hairflag & 0x02) != 0)
        {
            _facialHair?.Serialize(writer);
        }

        writer.Write(Race);

        writer.Write(m_TithingPoints);

        writer.Write(Corpse);

        writer.Write(CantWalk);

        // VirtueInfo.Serialize(writer, Virtues);

        writer.Write(Thirst);
        writer.Write(BAC);

        writer.Write(m_FollowersMax);

        writer.Write(MagicDamageAbsorb);

        writer.Write(GuildFealty);

        writer.Write(m_Guild);

        writer.Write(m_DisplayGuildTitle);

        writer.Write(CanSwim);

        writer.Write(Squelched);

        writer.Write(m_Holding);

        writer.Write(m_VirtualArmor);

        writer.Write(BaseSoundID);

        writer.Write(DisarmReady);
        writer.Write(StunReady);

        writer.Write(m_StatCap);

        writer.Write(NameHue);

        writer.Write(m_Hunger);

        writer.Write(m_Location);
        writer.Write(m_Body);
        writer.Write(m_Name);
        writer.Write(m_GuildTitle);
        writer.Write(m_Criminal);
        writer.Write(m_Kills);
        writer.Write(SpeechHue);
        writer.Write(EmoteHue);
        writer.Write(WhisperHue);
        writer.Write(YellHue);
        writer.Write(m_Language);
        writer.Write(m_Female);
        writer.Write(m_Warmode);
        writer.Write(m_Hidden);
        writer.Write((byte)m_Direction);
        writer.Write(m_Hue);
        writer.Write(m_Str);
        writer.Write(m_Dex);
        writer.Write(m_Int);
        writer.Write(m_Hits);
        writer.Write(m_Stam);
        writer.Write(m_Mana);

        writer.Write(m_Map);

        writer.Write(m_Blessed);
        writer.Write(m_Fame);
        writer.Write(m_Karma);
        writer.Write((byte)m_AccessLevel);
        Skills.Serialize(writer);

        writer.Write(Items);

        writer.Write(m_Player);
        writer.Write(m_Title);
        writer.Write(Profile);
        writer.Write(ProfileLocked);
        writer.Write(AutoPageNotify);

        writer.Write(LogoutLocation);
        writer.Write(LogoutMap);

        writer.Write((byte)m_StrLock);
        writer.Write((byte)m_DexLock);
        writer.Write((byte)m_IntLock);
    }

    public bool Deleted { get; private set; }

    public virtual void Delete()
    {
        if (Deleted)
        {
            return;
        }

        if (m_NetState != null)
        {
            m_NetState.CancelAllTrades();
            m_NetState.Disconnect($"Player {this} has been deleted.");
        }

        DropHolding();

        Region.OnRegionChange(this, m_Region, null);

        m_Region = null;
        // Is the above line REALLY needed?  The old Region system did NOT have said line
        // and worked fine, because of this a LOT of extra checks have to be done everywhere...
        // I guess this should be there for Garbage collection purposes, but, still, is it /really/ needed?

        OnDelete();

        for (var i = Items.Count - 1; i >= 0; --i)
        {
            if (i < Items.Count)
            {
                Items[i].OnParentDeleted(this);
            }
        }

        SendRemovePacket();

        m_Guild?.OnDelete(this);

        Deleted = true;

        m_Map?.OnLeave(this);
        m_Map = null;

        _hair = null;
        _facialHair = null;
        m_MountItem = null;

        World.RemoveEntity(this);

        OnAfterDelete();

        m_PropertyList = null;
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public Map Map
    {
        get => m_Map;
        set
        {
            if (Deleted)
            {
                return;
            }

            if (m_Map != value)
            {
                m_NetState?.ValidateAllTrades();

                var oldMap = m_Map;

                if (m_Map != null)
                {
                    m_Map.OnLeave(this);

                    ClearScreen();
                    SendRemovePacket();
                }

                for (var i = 0; i < Items.Count; ++i)
                {
                    Items[i].Map = value;
                }

                m_Map = value;
                InternalMapChange(oldMap);
            }
        }
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public Point3D Location
    {
        get => m_Location;
        set => SetLocation(value, true);
    }

    private void InternalMapChange(Map oldMap)
    {
        var ns = m_NetState;
        m_Map?.OnEnter(this);
        UpdateRegion();

        if (ns != null)
        {
            ns.Sequence = 0;

            if (m_Map != null)
            {
                ns.SendMapChange(Map);

                ns.SendMapPatches();

                ns.SendSeasonChange((byte)GetSeason(), true);

                ns.SendMobileUpdate(this);
                ns.SendServerChange(m_Location, m_Map);
            }

            ns.SendMobileIncoming(this, this);

            ns.SendMobileUpdate(this);
            CheckLightLevels(true);
            ns.SendMobileUpdate(this);
        }

        SendEverything();
        SendIncomingPacket();

        ns.SendMobileIncoming(this, this);
        ns.SendSupportedFeature();
        ns.SendMobileUpdate(this);
        ns.SendMobileAttributes(this);

        OnMapChange(oldMap);
    }

    public virtual void MoveToWorld(Point3D newLocation, Map map)
    {
        if (Deleted)
        {
            return;
        }

        if (m_Map == map)
        {
            SetLocation(newLocation, true);
            return;
        }

        var box = FindBankNoCreate();

        if (box?.Opened == true)
        {
            box.Close();
        }

        var oldLocation = m_Location;
        var oldMap = m_Map;

        if (oldMap != null)
        {
            oldMap.OnLeave(this);

            ClearScreen();
            SendRemovePacket();
        }

        for (var i = 0; i < Items.Count; ++i)
        {
            Items[i].Map = map;
        }

        m_Map = map;

        m_Location = newLocation;
        InternalMapChange(oldMap);
        OnLocationChange(oldLocation);

        m_Region?.OnLocationChanged(this, oldLocation);
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public int X
    {
        get => m_Location.m_X;
        set => Location = new Point3D(value, m_Location.m_Y, m_Location.m_Z);
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public int Y
    {
        get => m_Location.m_Y;
        set => Location = new Point3D(m_Location.m_X, value, m_Location.m_Z);
    }

    [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
    public int Z
    {
        get => m_Location.m_Z;
        set => Location = new Point3D(m_Location.m_X, m_Location.m_Y, value);
    }

    public virtual void ProcessDelta()
    {
        var delta = m_DeltaFlags;
        if (delta == MobileDelta.None)
        {
            return;
        }

        var attrs = delta & MobileDelta.Attributes;

        m_DeltaFlags = MobileDelta.None;
        m_InDeltaQueue = false;

        bool sendHits = false, sendStam = false, sendMana = false, sendAll = false, sendAny = false;
        bool sendIncoming = false, sendNonlocalIncoming = false;
        bool sendUpdate = false, sendRemove = false;
        bool sendPublicStats = false, sendPrivateStats = false;
        bool sendMoving = false, sendNonlocalMoving = false;
        var sendOPLUpdate = ObjectPropertyList.Enabled && (delta & MobileDelta.Properties) != 0;

        bool sendHair = false, sendFacialHair = false, removeHair = false, removeFacialHair = false;

        bool sendHealthbarPoison = false, sendHealthbarYellow = false;

        if (attrs != MobileDelta.None)
        {
            sendAny = true;

            if (attrs == MobileDelta.Attributes)
            {
                sendAll = true;
            }
            else
            {
                sendHits = (attrs & MobileDelta.Hits) != 0;
                sendStam = (attrs & MobileDelta.Stam) != 0;
                sendMana = (attrs & MobileDelta.Mana) != 0;
            }
        }

        if ((delta & MobileDelta.GhostUpdate) != 0)
        {
            sendNonlocalIncoming = true;
        }

        if ((delta & MobileDelta.Hue) != 0)
        {
            sendNonlocalIncoming = true;
            sendUpdate = true;
            sendRemove = true;
        }

        if ((delta & MobileDelta.Direction) != 0)
        {
            sendNonlocalMoving = true;
            sendUpdate = true;
        }

        if ((delta & MobileDelta.Body) != 0)
        {
            sendUpdate = true;
            sendIncoming = true;
        }

        if ((delta & (MobileDelta.Flags | MobileDelta.Noto)) != 0)
        {
            sendMoving = true;
        }

        if ((delta & MobileDelta.HealthbarPoison) != 0)
        {
            sendHealthbarPoison = true;
        }

        if ((delta & MobileDelta.HealthbarYellow) != 0)
        {
            sendHealthbarYellow = true;
        }

        if ((delta & MobileDelta.Name) != 0)
        {
            sendAll = false;
            sendHits = false;
            sendAny = sendStam || sendMana;
            sendPublicStats = true;
        }

        if ((delta & (MobileDelta.WeaponDamage | MobileDelta.Resistances | MobileDelta.Stat |
                      MobileDelta.Weight | MobileDelta.Gold | MobileDelta.Armor | MobileDelta.StatCap |
                      MobileDelta.Followers | MobileDelta.TithingPoints | MobileDelta.Race)) != 0)
        {
            sendPrivateStats = true;
        }

        if ((delta & MobileDelta.Hair) != 0)
        {
            if (HairItemID <= 0)
            {
                removeHair = true;
            }

            sendHair = true;
        }

        if ((delta & MobileDelta.FacialHair) != 0)
        {
            if (FacialHairItemID <= 0)
            {
                removeFacialHair = true;
            }

            sendFacialHair = true;
        }

        var hairSerial = Hair.VirtualSerial;
        var hairLength = removeHair
            ? OutgoingVirtualHairPackets.RemovePacketLength
            : OutgoingVirtualHairPackets.EquipUpdatePacketLength;

        Span<byte> hairPacket = stackalloc byte[hairLength].InitializePacket();

        var facialHairSerial = FacialHair.VirtualSerial;
        var facialHairLength = removeFacialHair
            ? OutgoingVirtualHairPackets.RemovePacketLength
            : OutgoingVirtualHairPackets.EquipUpdatePacketLength;

        Span<byte> facialHairPacket = stackalloc byte[facialHairLength].InitializePacket();

        const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;

        Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
        mobileMovingCache.Clear();

        var ourState = m_NetState;

        if (ourState != null)
        {
            if (sendUpdate)
            {
                ourState.Sequence = 0;
                ourState.SendMobileUpdate(this);
            }

            if (sendIncoming)
            {
                ourState.SendMobileIncoming(this, this);
            }

            if (sendMoving || !ourState.StygianAbyss && (sendHealthbarPoison || sendHealthbarYellow))
            {
                ourState.SendMobileMovingUsingCache(mobileMovingCache, this, this);
            }

            if (ourState.StygianAbyss)
            {
                if (sendHealthbarPoison)
                {
                    ourState.SendMobileHealthbar(this, Healthbar.Poison);
                }

                if (sendHealthbarYellow)
                {
                    ourState.SendMobileHealthbar(this, Healthbar.Yellow);
                }
            }

            if (sendPublicStats || sendPrivateStats)
            {
                ourState.SendMobileStatus(this);
            }
            else if (sendAll)
            {
                ourState.SendMobileAttributes(this);
            }
            else if (sendAny)
            {
                if (sendHits)
                {
                    ourState.SendMobileHits(this);
                }

                if (sendMana)
                {
                    ourState.SendMobileMana(this);
                }

                if (sendStam)
                {
                    ourState.SendMobileStam(this);
                }
            }

            if (sendStam || sendMana)
            {
                if (Party is IParty ip)
                {
                    if (sendMana)
                    {
                        ip.OnManaChanged(this);
                    }

                    if (sendStam)
                    {
                        ip.OnStamChanged(this);
                    }
                }
            }

            if (sendHair)
            {
                if (removeHair)
                {
                    OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial);
                }
                else
                {
                    OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
                        hairPacket,
                        this,
                        (uint)hairSerial,
                        HairItemID,
                        HairHue,
                        Layer.Hair
                    );
                }

                ourState.Send(hairPacket);
            }

            if (sendFacialHair)
            {
                if (removeFacialHair)
                {
                    OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial);
                }
                else
                {
                    OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
                        facialHairPacket,
                        this,
                        (uint)facialHairSerial,
                        FacialHairItemID,
                        FacialHairHue,
                        Layer.FacialHair
                    );
                }
                ourState.Send(facialHairPacket);
            }

            if (sendOPLUpdate)
            {
                SendOPLPacketTo(ourState);
            }
        }

        // TODO: Is it even valid to send packets to our state while we have a null map? Look into failing fast
        if (m_Map == null)
        {
            return;
        }

        sendMoving = sendMoving || sendNonlocalMoving;
        sendIncoming = sendIncoming || sendNonlocalIncoming;
        sendHits = sendHits || sendAll;

        if (!(sendRemove || sendIncoming || sendPublicStats || sendHits || sendMoving ||
              sendOPLUpdate || sendHair || sendFacialHair || sendHealthbarPoison ||
              sendHealthbarYellow))
        {
            return;
        }

        Span<byte> statBufferTrue = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket();
        Span<byte> statBufferFalse = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket();
        Span<byte> hbpBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket();
        Span<byte> hbyBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket();
        Span<byte> deadBuffer = stackalloc byte[OutgoingMobilePackets.BondedStatusPacketLength].InitializePacket();
        Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();
        Span<byte> hitsPacket = stackalloc byte[OutgoingMobilePackets.MobileAttributePacketLength].InitializePacket();

        foreach (var state in Map.GetClientsInRange(m_Location))
        {
            var beholder = state.Mobile;

            if (beholder == this || !beholder.CanSee(this))
            {
                continue;
            }

            if (sendRemove)
            {
                OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
                state.Send(removeEntity);
            }

            if (sendIncoming)
            {
                state.SendMobileIncoming(beholder, this);

                if (IsDeadBondedPet)
                {
                    OutgoingMobilePackets.CreateBondedStatus(deadBuffer, Serial, true);
                    state.Send(deadBuffer);
                }
            }

            if (sendMoving || !state.StygianAbyss && (sendHealthbarPoison || sendHealthbarYellow))
            {
                state.SendMobileMovingUsingCache(mobileMovingCache, beholder, this);
            }

            if (state.StygianAbyss)
            {
                if (sendHealthbarPoison)
                {
                    OutgoingMobilePackets.CreateMobileHealthbar(hbpBuffer, this, Healthbar.Poison);
                    state.Send(hbpBuffer);
                }

                if (sendHealthbarYellow)
                {
                    OutgoingMobilePackets.CreateMobileHealthbar(hbyBuffer, this, Healthbar.Yellow);
                    state.Send(hbyBuffer);
                }
            }

            if (sendPublicStats)
            {
                if (CanBeRenamedBy(beholder))
                {
                    OutgoingMobilePackets.CreateMobileStatusCompact(statBufferTrue, this, true);
                    state.Send(statBufferTrue);
                }
                else
                {
                    OutgoingMobilePackets.CreateMobileStatusCompact(statBufferFalse, this, false);
                    state.Send(statBufferFalse);
                }
            }
            else if (sendHits)
            {
                OutgoingMobilePackets.CreateMobileHits(hitsPacket, this, true);
                state.Send(hitsPacket);
            }

            if (sendHair)
            {
                if (removeHair)
                {
                    OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial);
                }
                else
                {
                    OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
                        hairPacket,
                        this,
                        (uint)hairSerial,
                        HairItemID,
                        HairHue,
                        Layer.Hair
                    );
                }

                state.Send(hairPacket);
            }

            if (sendFacialHair)
            {
                if (removeFacialHair)
                {
                    OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial);
                }
                else
                {
                    OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket(
                        facialHairPacket,
                        this,
                        (uint)facialHairSerial,
                        FacialHairItemID,
                        FacialHairHue,
                        Layer.FacialHair
                    );
                }
                state.Send(facialHairPacket);
            }

            SendOPLPacketTo(state);
        }
    }

    public ISpawner Spawner { get; set; }

    public virtual void OnBeforeSpawn(Point3D location, Map m)
    {
    }

    public virtual void OnAfterSpawn()
    {
    }

    public virtual bool InRange(Point2D p, int range) =>
        p.m_X >= Location.m_X - range
        && p.m_X <= Location.m_X + range
        && p.m_Y >= Location.m_Y - range
        && p.m_Y <= Location.m_Y + range;

    public virtual bool InRange(Point3D p, int range) =>
        p.m_X >= Location.m_X - range
        && p.m_X <= Location.m_X + range
        && p.m_Y >= Location.m_Y - range
        && p.m_Y <= Location.m_Y + range;

    public virtual bool InRange(IPoint2D p, int range) =>
        p.X >= Location.m_X - range
        && p.X <= Location.m_X + range
        && p.Y >= Location.m_Y - range
        && p.Y <= Location.m_Y + range;

    protected virtual void OnRaceChange(Race oldRace)
    {
    }

    public virtual void ComputeLightLevels(out int global, out int personal)
    {
        ComputeBaseLightLevels(out global, out personal);

        m_Region?.AlterLightLevel(this, ref global, ref personal);
    }

    public virtual void ComputeBaseLightLevels(out int global, out int personal)
    {
        global = 0;
        personal = m_LightLevel;
    }

    public virtual void CheckLightLevels(bool forceResend)
    {
    }

    public virtual void UpdateResistances()
    {
        var delta = false;

        if (Resistances == null)
        {
            Resistances = new[] { int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue };
        }
        else
        {
            for (var i = 0; i < Resistances.Length; ++i)
            {
                if (Resistances[i] != int.MinValue)
                {
                    Resistances[i] = int.MinValue;
                    delta = true;
                }
            }
        }

        if (delta)
        {
            Delta(MobileDelta.Resistances);
        }
    }

    public virtual int GetResistance(ResistanceType type)
    {
        var v = (int)type;

        if (Resistances == null || Resistances.Length == 0 || v < 0 || v >= Resistances.Length)
        {
            return 0;
        }

        var res = Resistances[v];

        if (res == int.MinValue)
        {
            ComputeResistances();
            res = Resistances[v];
        }

        return res;
    }

    public virtual void AddResistanceMod(ResistanceMod mod)
    {
        if (mod == null)
        {
            return;
        }

        _resistanceMods ??= new List<ResistanceMod>();
        _resistanceMods.Add(mod);

        UpdateResistances();
    }

    public virtual ResistanceMod GetResistanceMod(string name)
    {
        if (_resistanceMods == null || name == null)
        {
            return null;
        }

        for (var i = 0; i < _resistanceMods.Count; i++)
        {
            var mod = _resistanceMods[i];
            if (mod.Name == name)
            {
                return mod;
            }
        }

        return null;
    }

    public virtual void RemoveResistanceMod(string name)
    {
        if (_resistanceMods == null || name == null)
        {
            return;
        }

        for (var i = _resistanceMods.Count - 1; i >= 0; i--)
        {
            var mod = _resistanceMods[i];
            if (mod.Name == name)
            {
                _resistanceMods.RemoveAt(i);
            }
        }

        if (_resistanceMods.Count == 0)
        {
            _resistanceMods = null;
        }

        UpdateResistances();
    }

    public virtual void RemoveResistanceMod(ResistanceMod mod)
    {
        if (_resistanceMods == null)
        {
            return;
        }

        _resistanceMods.Remove(mod);

        if (_resistanceMods.Count == 0)
        {
            _resistanceMods = null;
        }

        UpdateResistances();
    }

    public virtual void ComputeResistances()
    {
        Resistances ??= new[] { int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue };

        for (var i = 0; i < Resistances.Length; ++i)
        {
            Resistances[i] = 0;
        }

        Resistances[0] += BasePhysicalResistance;
        Resistances[1] += BaseFireResistance;
        Resistances[2] += BaseColdResistance;
        Resistances[3] += BasePoisonResistance;
        Resistances[4] += BaseEnergyResistance;

        if (_resistanceMods != null)
        {
            for (var i = 0; i < _resistanceMods.Count; i++)
            {
                var mod = _resistanceMods[i];
                var v = (int)mod.Type;

                if (v >= 0 && v < Resistances.Length)
                {
                    Resistances[v] += mod.Offset;
                }
            }
        }

        for (var i = 0; i < Items.Count; ++i)
        {
            var item = Items[i];

            if (item.CheckPropertyConflict(this))
            {
                continue;
            }

            Resistances[0] += item.PhysicalResistance;
            Resistances[1] += item.FireResistance;
            Resistances[2] += item.ColdResistance;
            Resistances[3] += item.PoisonResistance;
            Resistances[4] += item.EnergyResistance;
        }

        for (var i = 0; i < Resistances.Length; ++i)
        {
            var min = GetMinResistance((ResistanceType)i);
            var max = GetMaxResistance((ResistanceType)i);

            if (max < min)
            {
                max = min;
            }

            if (Resistances[i] > max)
            {
                Resistances[i] = max;
            }
            else if (Resistances[i] < min)
            {
                Resistances[i] = min;
            }
        }
    }

    public virtual int GetMinResistance(ResistanceType type) => int.MinValue;

    public virtual int GetMaxResistance(ResistanceType type) => m_Player ? MaxPlayerResistance : int.MaxValue;

    public int GetAOSStatus(int index) => AOSStatusHandler?.Invoke(this, index) ?? 0;

    public virtual void SendPropertiesTo(NetState ns)
    {
        ns?.Send(PropertyList.Buffer);
    }

    public virtual void OnAosSingleClick(Mobile from)
    {
        var opl = PropertyList;

        if (opl.Header > 0)
        {
            int hue;

            if (NameHue != -1)
            {
                hue = NameHue;
            }
            else if (m_AccessLevel > AccessLevel.Player)
            {
                hue = 11;
            }
            else
            {
                hue = Notoriety.GetHue(Notoriety.Compute(from, this));
            }

            from.NetState.SendMessageLocalized(Serial, Body, MessageType.Label, hue, 3, opl.Header, Name, opl.HeaderArgs);
        }
    }

    public virtual string ApplyNameSuffix(string suffix) => suffix;

    public virtual void AddNameProperties(IPropertyList list)
    {
        string prefix;

        if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000)
        {
            prefix = m_Female ? "Lady " : "Lord ";
        }
        else
        {
            prefix = " ";
        }

        var guild = m_Guild;
        var hasTitle = PropertyTitle && !string.IsNullOrEmpty(Title);
        var hasGuild = guild != null && (m_Player || m_DisplayGuildTitle);

        string suffix = hasTitle switch
        {
            true when hasGuild  => $" {Title} [{guild.Abbreviation.FixHtmlFormattable()}]",
            true                => $" {Title}",
            false when hasGuild => $" [{guild.Abbreviation.FixHtmlFormattable()}]",
            _                   => " "
        };

        list.Add(1050045, $"{prefix}\t{Name ?? " "}\t{ApplyNameSuffix(suffix)}"); // ~1_PREFIX~~2_NAME~~3_SUFFIX~

        if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular))
        {
            var type = guild.Type >= 0 && (int)guild.Type < m_GuildTypes.Length ? m_GuildTypes[(int)guild.Type] : "";

            var guildTitle = GuildTitle?.Trim() ?? "";

            if (guildTitle.Length > 0)
            {
                if (NewGuildDisplay)
                {
                    list.Add($"{guildTitle.FixHtmlFormattable()}, {guild.Name.FixHtmlFormattable()}");
                }
                else
                {
                    list.Add($"{guildTitle.FixHtmlFormattable()}, {guild.Name.FixHtmlFormattable()} Guild{type}");
                }
            }
            else
            {
                list.Add(guild.Name.FixHtml());
            }
        }
    }

    public virtual void GetChildProperties(IPropertyList list, Item item)
    {
    }

    public virtual void GetChildNameProperties(IPropertyList list, Item item)
    {
    }

    private void UpdateAggrExpire()
    {
        if (Deleted || Aggressors.Count == 0 && Aggressed.Count == 0)
        {
            StopAggrExpire();
        }
        else if (!_expireAggrTimerToken.Running)
        {
            Timer.StartTimer(ExpireAggressorsDelay, ExpireAggressorsDelay, ExpireAggr, out _expireAggrTimerToken);
        }
    }

    private void ExpireAggr()
    {
        if (Deleted || Aggressors.Count == 0 && Aggressed.Count == 0)
        {
            StopAggrExpire();
        }
        else
        {
            CheckAggrExpire();
        }
    }

    private void StopAggrExpire()
    {
        _expireAggrTimerToken.Cancel();
    }

    private void CheckAggrExpire()
    {
        for (var i = Aggressors.Count - 1; i >= 0; --i)
        {
            if (i >= Aggressors.Count)
            {
                continue;
            }

            var info = Aggressors[i];

            if (info.Expired)
            {
                var attacker = info.Attacker;
                attacker.RemoveAggressed(this);

                Aggressors.RemoveAt(i);
                info.Free();

                if (m_NetState != null && CanSee(attacker) && Utility.InUpdateRange(m_Location, attacker.m_Location))
                {
                    m_NetState.SendMobileIncoming(this, attacker);
                }
            }
        }

        for (var i = Aggressed.Count - 1; i >= 0; --i)
        {
            if (i >= Aggressed.Count)
            {
                continue;
            }

            var info = Aggressed[i];

            if (info.Expired)
            {
                var defender = info.Defender;
                defender.RemoveAggressor(this);

                Aggressed.RemoveAt(i);
                info.Free();

                if (m_NetState != null && CanSee(defender) && Utility.InUpdateRange(m_Location, defender.m_Location))
                {
                    m_NetState.SendMobileIncoming(this, defender);
                }
            }
        }

        UpdateAggrExpire();
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="skill" /> changes in some way.
    /// </summary>
    public virtual void OnSkillInvalidated(Skill skill)
    {
    }

    public virtual void ValidateSkillMods()
    {
        if (_skillMods == null)
        {
            return;
        }

        for (var i = _skillMods.Count - 1; i >= 0; i--)
        {
            var mod = _skillMods[i];

            if (!mod.CheckCondition())
            {
                _skillMods.RemoveAt(i);
                mod.Owner = null;
                Skills[mod.Skill]?.Update();
            }
        }

    }

    public virtual void AddSkillMod(SkillMod mod)
    {
        if (mod == null)
        {
            return;
        }

        ValidateSkillMods();
        _skillMods ??= new List<SkillMod>();
        _skillMods.Add(mod);
        mod.Owner = this;
        Skills[mod.Skill]?.Update();
    }

    public virtual SkillMod GetSkillMod(string name)
    {
        if (_skillMods == null || name == null)
        {
            return null;
        }

        ValidateSkillMods();

        for (var i = 0; i < _skillMods.Count; i++)
        {
            var mod = _skillMods[i];
            if (mod.Name == name)
            {
                return mod;
            }
        }

        return null;
    }

    public virtual void RemoveSkillMod(string name)
    {
        if (_skillMods == null || name == null)
        {
            return;
        }

        for (var i = _skillMods.Count - 1; i >= 0; i--)
        {
            var mod = _skillMods[i];
            if (mod.Name == name)
            {
                mod.Owner = null;
                Skills[mod.Skill]?.Update();
                _skillMods.RemoveAt(i);
            }
        }

        ValidateSkillMods();

        if (_skillMods.Count == 0)
        {
            _skillMods = null;
        }
    }

    public virtual void RemoveSkillMod(SkillMod mod)
    {
        if (_skillMods == null || mod == null)
        {
            return;
        }

        if (_skillMods.Remove(mod))
        {
            mod.Owner = null;
            Skills[mod.Skill]?.Update();
        }

        ValidateSkillMods();

        if (_skillMods.Count == 0)
        {
            _skillMods = null;
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when a client, <paramref name="from" />, invokes a 'help request' for the Mobile.
    ///     Seemingly no longer functional in newer clients.
    /// </summary>
    public virtual void OnHelpRequest(Mobile from)
    {
    }

    public void DelayChangeWarmode(bool value)
    {
        if (m_WarmodeChanges > WarmodeCatchCount)
        {
            _warmodeSpamValue = value;
            m_WarmodeChanges++;
            return;
        }

        if (m_Warmode == value)
        {
            return;
        }

        var now = Core.Now;
        var next = m_NextWarmodeChange;

        if (now > next || m_WarmodeChanges == 0)
        {
            m_WarmodeChanges = 1;
            m_NextWarmodeChange = now + WarmodeSpamCatch;
        }
        else if (m_WarmodeChanges++ == WarmodeCatchCount)
        {
            Timer.StartTimer(WarmodeSpamDelay, WarmodeSpamTimeout);
            return;
        }

        Warmode = value;
    }

    private void WarmodeSpamTimeout()
    {
        Warmode = _warmodeSpamValue;
        m_WarmodeChanges = 0;
    }

    public bool InLOS(Mobile target) =>
        !Deleted && m_Map != null &&
        (target == this || m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));

    public bool InLOS(object target) =>
        !Deleted && m_Map != null &&
        (target == this || m_AccessLevel > AccessLevel.Player || target is Item item && item.RootParent == this
         || m_Map.LineOfSight(this, target));

    public bool InLOS(Point3D target) =>
        !Deleted && m_Map != null && (m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));

    public bool AtPoint(int x, int y) => m_Location.m_X == x && m_Location.m_Y == y;

    public bool BeginAction<T>() => BeginAction(typeof(T));

    public bool BeginAction(object toLock)
    {
        if (_actions == null)
        {
            _actions = new List<object> { toLock };
            return true;
        }

        if (!_actions.Contains(toLock))
        {
            _actions.Add(toLock);
            return true;
        }

        return false;
    }

    public bool CanBeginAction<T>() => CanBeginAction(typeof(T));

    public bool CanBeginAction(object toLock) => _actions?.Contains(toLock) != true;

    public void EndAction<T>() => EndAction(typeof(T));

    public void EndAction(object toLock)
    {
        if (_actions != null)
        {
            _actions.Remove(toLock);

            if (_actions.Count == 0)
            {
                _actions = null;
            }
        }
    }

    public virtual TimeSpan GetLogoutDelay() => Region.GetLogoutDelay(this);

    private void ExpireParalyzed()
    {
        Paralyzed = false;
    }

    public virtual void Paralyze(TimeSpan duration)
    {
        if (!m_Paralyzed)
        {
            Paralyzed = true;
            Timer.StartTimer(duration, ExpireParalyzed, out _paraTimerToken);
        }
    }

    private void ExpireFrozen()
    {
        Frozen = false;
    }

    public virtual void Freeze(TimeSpan duration)
    {
        if (!m_Frozen)
        {
            Frozen = true;
            Timer.StartTimer(duration, ExpireFrozen, out _frozenTimerToken);
        }
    }

    public override string ToString() => $"{Serial} \"{Name}\"";

    public virtual void SendSkillMessage()
    {
        if (NextActionMessage - Core.TickCount >= 0)
        {
            return;
        }

        NextActionMessage = Core.TickCount + ActionMessageDelay;

        SendLocalizedMessage(500118); // You must wait a few moments to use another skill.
    }

    public virtual void SendActionMessage()
    {
        if (NextActionMessage - Core.TickCount >= 0)
        {
            return;
        }

        NextActionMessage = Core.TickCount + ActionMessageDelay;

        SendLocalizedMessage(500119); // You must wait to perform another action.
    }

    public virtual void ClearHands()
    {
        ClearHand(FindItemOnLayer(Layer.OneHanded));
        ClearHand(FindItemOnLayer(Layer.TwoHanded));
    }

    public virtual void ClearHand(Item item)
    {
        if (item?.Movable == true && !item.AllowEquippedCast(this))
        {
            var pack = Backpack;

            if (pack == null)
            {
                AddToBackpack(item);
            }
            else
            {
                pack.DropItem(item);
            }
        }
    }

    public virtual void Attack(Mobile m)
    {
        if (CheckAttack(m))
        {
            Combatant = m;
        }
    }

    public virtual bool CheckAttack(Mobile m) => Utility.InUpdateRange(Location, m.Location) && CanSee(m) && InLOS(m);

    /// <summary>
    ///     Overridable. Virtual event invoked after the <see cref="Combatant" /> property has changed.
    ///     <seealso cref="Combatant" />
    /// </summary>
    public virtual void OnCombatantChange()
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked after the <see cref="Hunger" /> property has changed.
    ///     <seealso cref="Hunger" />
    /// </summary>
    public virtual void OnHungerChanged(int oldValue)
    {
    }

    public double GetDistanceToSqrt(Point3D p)
    {
        var xDelta = m_Location.m_X - p.m_X;
        var yDelta = m_Location.m_Y - p.m_Y;

        return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
    }

    public double GetDistanceToSqrt(Mobile m)
    {
        var xDelta = m_Location.m_X - m.m_Location.m_X;
        var yDelta = m_Location.m_Y - m.m_Location.m_Y;

        return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
    }

    public double GetDistanceToSqrt(Point2D p)
    {
        var xDelta = m_Location.m_X - p.X;
        var yDelta = m_Location.m_Y - p.Y;

        return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
    }

    public double GetDistanceToSqrt(IPoint2D p)
    {
        var xDelta = m_Location.m_X - p.X;
        var yDelta = m_Location.m_Y - p.Y;

        return Math.Sqrt(xDelta * xDelta + yDelta * yDelta);
    }

    public virtual void AggressiveAction(Mobile aggressor) => AggressiveAction(aggressor, false);

    public virtual void AggressiveAction(Mobile aggressor, bool criminal)
    {
        if (aggressor == this)
        {
            return;
        }

        var args = AggressiveActionEventArgs.Create(this, aggressor, criminal);

        EventSink.InvokeAggressiveAction(args);

        args.Free();

        if (Combatant == aggressor)
        {
            _expireCombatantTimerToken.Cancel();
            Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
        }

        var addAggressor = true;

        var list = Aggressors;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Attacker == aggressor)
            {
                info.Refresh();
                info.CriminalAggression = criminal;
                info.CanReportMurder = criminal;

                addAggressor = false;
            }
        }

        list = aggressor.Aggressors;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Attacker == this)
            {
                info.Refresh();

                addAggressor = false;
            }
        }

        var addAggressed = true;

        list = Aggressed;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Defender == aggressor)
            {
                info.Refresh();

                addAggressed = false;
            }
        }

        list = aggressor.Aggressed;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Defender == this)
            {
                info.Refresh();
                info.CriminalAggression = criminal;
                info.CanReportMurder = criminal;

                addAggressed = false;
            }
        }

        var setCombatant = false;

        if (addAggressor)
        {
            Aggressors.Add(
                AggressorInfo.Create(
                    aggressor,
                    this,
                    criminal
                )
            );

            if (CanSee(aggressor))
            {
                m_NetState.SendMobileIncoming(this, aggressor);
            }

            if (Combatant == null)
            {
                setCombatant = true;
            }

            UpdateAggrExpire();
        }

        if (addAggressed)
        {
            aggressor.Aggressed.Add(
                AggressorInfo.Create(
                    aggressor,
                    this,
                    criminal
                )
            );

            if (CanSee(aggressor))
            {
                m_NetState.SendMobileIncoming(this, aggressor);
            }

            if (Combatant == null)
            {
                setCombatant = true;
            }

            UpdateAggrExpire();
        }

        if (setCombatant)
        {
            Combatant = aggressor;
        }

        Region.OnAggressed(aggressor, this, criminal);
    }

    public void RemoveAggressed(Mobile aggressed)
    {
        if (Deleted)
        {
            return;
        }

        var list = Aggressed;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Defender == aggressed)
            {
                Aggressed.RemoveAt(i);
                info.Free();

                if (m_NetState != null && CanSee(aggressed))
                {
                    m_NetState.SendMobileIncoming(this, aggressed);
                }

                break;
            }
        }

        UpdateAggrExpire();
    }

    public void RemoveAggressor(Mobile aggressor)
    {
        if (Deleted)
        {
            return;
        }

        var list = Aggressors;

        for (var i = 0; i < list.Count; ++i)
        {
            var info = list[i];

            if (info.Attacker == aggressor)
            {
                Aggressors.RemoveAt(i);
                info.Free();

                if (m_NetState != null && CanSee(aggressor))
                {
                    m_NetState.SendMobileIncoming(this, aggressor);
                }

                break;
            }
        }

        UpdateAggrExpire();
    }

    public virtual int GetTotal(TotalType type) =>
        type switch
        {
            TotalType.Gold   => m_TotalGold,
            TotalType.Items  => m_TotalItems,
            TotalType.Weight => m_TotalWeight,
            _                => 0
        };

    public virtual void UpdateTotal(Item sender, TotalType type, int delta)
    {
        if (delta == 0 || sender.IsVirtualItem)
        {
            return;
        }

        switch (type)
        {
            default:
                {
                    m_TotalGold += delta;
                    Delta(MobileDelta.Gold);
                    break;
                }

            case TotalType.Items:
                {
                    m_TotalItems += delta;
                    break;
                }

            case TotalType.Weight:
                {
                    m_TotalWeight += delta;
                    Delta(MobileDelta.Weight);
                    OnWeightChange(m_TotalWeight - delta);
                    break;
                }
        }
    }

    public virtual void UpdateTotals()
    {
        if (Items == null)
        {
            return;
        }

        var oldWeight = m_TotalWeight;

        m_TotalGold = 0;
        m_TotalItems = 0;
        m_TotalWeight = 0;

        for (var i = 0; i < Items.Count; ++i)
        {
            var item = Items[i];

            item.UpdateTotals();

            if (item.IsVirtualItem)
            {
                continue;
            }

            m_TotalGold += item.TotalGold;
            m_TotalItems += item.TotalItems + 1;
            m_TotalWeight += item.TotalWeight + item.PileWeight;
        }

        if (m_Holding != null)
        {
            m_TotalWeight += m_Holding.TotalWeight + m_Holding.PileWeight;
        }

        if (m_TotalWeight != oldWeight)
        {
            OnWeightChange(oldWeight);
        }
    }

    public void ClearTarget() => m_Target = null;

    public Target BeginTarget(int range, bool allowGround, TargetFlags flags, TargetCallback callback) =>
        Target = new SimpleTarget(range, flags, allowGround, callback);

    public Target BeginTarget<T>(
        int range, bool allowGround, TargetFlags flags, TargetStateCallback<T> callback,
        T state
    ) =>
        Target = new SimpleStateTarget<T>(range, flags, allowGround, callback, state);

    /// <summary>
    ///     Overridable. Virtual event invoked after the <see cref="Target">Target property</see> has changed.
    /// </summary>
    protected virtual void OnTargetChange()
    {
    }

    public virtual bool CheckContextMenuDisplay(IEntity target) => true;

    private bool InternalOnMove(Direction d)
    {
        if (!OnMove(d))
        {
            return false;
        }

        var e = MovementEventArgs.Create(this, d);

        EventSink.InvokeMovement(e);

        var ret = !e.Blocked;

        e.Free();

        return ret;
    }

    /// <summary>
    ///     Overridable. Event invoked before the Mobile <see cref="Move">moves</see>.
    /// </summary>
    /// <returns>True if the move is allowed, false if not.</returns>
    protected virtual bool OnMove(Direction d)
    {
        if (m_Hidden && m_AccessLevel == AccessLevel.Player)
        {
            if (AllowedStealthSteps-- <= 0 || (d & Direction.Running) != 0 || Mounted)
            {
                RevealingAction();
            }
        }

        return true;
    }

    public virtual bool CheckMovement(Direction d, out int newZ) => CalcMoves.CheckMovement(this, d, out newZ);

    private bool CanMove(Direction d, Point3D oldLocation, out Point3D newLocation)
    {
        newLocation = oldLocation;

        if (m_Spell?.OnCasterMoving(d) == false)
        {
            return false;
        }

        if (m_Paralyzed || m_Frozen)
        {
            SendLocalizedMessage(500111); // You are frozen and can not move.

            return false;
        }

        if (!CheckMovement(d, out var newZ))
        {
            return false;
        }

        var oldX = oldLocation.m_X;
        var oldY = oldLocation.m_Y;
        var oldZ = oldLocation.m_Z;
        var x = oldX;
        var y = oldY;
        CalcMoves.Offset(d, ref x, ref y);
        newLocation = new Point3D(x, y, newZ);

        Pushing = false;

        var map = m_Map;

        if (map == null || !Region.CanMove(this, d, newLocation, oldLocation, map))
        {
            return false;
        }

        var oldSector = map.GetSector(oldX, oldY);
        var newSector = map.GetSector(x, y);

        if (oldSector != newSector)
        {
            using var queue = PooledRefQueue<IEntity>.Create(2048);
            foreach (var m in oldSector.Mobiles)
            {
                if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
                {
                    queue.Enqueue(m);
                }
            }

            while (queue.Count > 0)
            {
                if (!queue.Dequeue().OnMoveOff(this))
                {
                    return false;
                }
            }

            foreach (var item in oldSector.Items)
            {
                if (item.AtWorldPoint(oldX, oldY) &&
                    (item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
                {
                    queue.Enqueue(item);
                }
            }

            while (queue.Count > 0)
            {
                if (!queue.Dequeue().OnMoveOff(this))
                {
                    return false;
                }
            }

            foreach (var m in newSector.Mobiles)
            {
                if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
                {
                    queue.Enqueue(m);
                }
            }

            while (queue.Count > 0)
            {
                if (!queue.Dequeue().OnMoveOver(this))
                {
                    return false;
                }
            }

            foreach (var item in newSector.Items)
            {
                if (item.AtWorldPoint(x, y) &&
                    (item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
                {
                    queue.Enqueue(item);
                }
            }

            while (queue.Count > 0)
            {
                if (!queue.Dequeue().OnMoveOver(this))
                {
                    return false;
                }
            }
        }
        else
        {
            using var queue = PooledRefQueue<(IEntity, byte)>.Create(2048);
            foreach (var m in oldSector.Mobiles)
            {
                byte flag;
                if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
                {
                    flag = 1;
                }
                else
                {
                    flag = 0;
                }

                if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
                {
                    flag += 2;
                }

                if (flag > 0)
                {
                    queue.Enqueue((m, flag));
                }
            }

            while (queue.Count > 0)
            {
                var (entity, flag) = queue.Dequeue();

                if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
                {
                    return false;
                }
            }

            foreach (var item in oldSector.Items)
            {
                byte flag;
                if (item.AtWorldPoint(oldX, oldY) &&
                    (item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
                {
                    flag = 1;
                }
                else
                {
                    flag = 0;
                }

                if (item.AtWorldPoint(x, y) &&
                    (item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
                {
                    flag += 2;
                }

                if (flag > 0)
                {
                    queue.Enqueue((item, flag));
                }
            }

            while (queue.Count > 0)
            {
                var (entity, flag) = queue.Dequeue();

                if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
                {
                    return false;
                }
            }
        }

        if (!InternalOnMove(d))
        {
            return false;
        }

        LastMoveTime = Core.TickCount;

        return true;
    }

    public virtual bool Move(Direction d)
    {
        if (Deleted)
        {
            return false;
        }

        var box = FindBankNoCreate();

        if (box?.Opened == true)
        {
            box.Close();
        }

        var oldLocation = m_Location;
        Point3D newLocation;

        if ((m_Direction & Direction.Mask) == (d & Direction.Mask))
        {
            // We are actually moving (not just a direction change)
            if (!CanMove(d, oldLocation, out newLocation))
            {
                return false;
            }

            DisruptiveAction();
        }
        else
        {
            newLocation = oldLocation;
        }

        if (m_NetState != null)
        {
            m_NetState._nextMovementTime += ComputeMovementSpeed(d);
            m_NetState.SendMovementAck(m_NetState.Sequence, this);
        }

        SetLocation(newLocation, false);
        SetDirection(d);

        if (m_Map != null)
        {
            using var moveQueue = PooledRefQueue<IEntity>.Create(2048);
            using var moveClientQueue = PooledRefQueue<Mobile>.Create(2048);

            foreach (var mob in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
            {
                if (mob == this)
                {
                    continue;
                }

                if (mob.NetState != null)
                {
                    moveClientQueue.Enqueue(mob);
                }

                moveQueue.Enqueue(mob);
            }

            foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
            {
                if (item.HandlesOnMovement)
                {
                    moveQueue.Enqueue(item);
                }
            }

            if (moveClientQueue.Count > 0)
            {
                const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;

                Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
                mobileMovingCache.Clear();

                while (moveClientQueue.Count > 0)
                {
                    var m = moveClientQueue.Dequeue();
                    var ns = m.NetState;

                    if (ns != null && Utility.InUpdateRange(m_Location, m.m_Location) && m.CanSee(this))
                    {
                        ns.SendMobileMovingUsingCache(mobileMovingCache, m, this);
                    }
                }
            }

            while (moveQueue.Count > 0)
            {
                moveQueue.Dequeue().OnMovement(this, oldLocation);
            }
        }

        OnAfterMove(oldLocation);

        return true;
    }

    public virtual void OnAfterMove(Point3D oldLocation)
    {
    }

    public int ComputeMovementSpeed() => ComputeMovementSpeed(Direction, false);

    public virtual int ComputeMovementSpeed(Direction dir, bool checkTurning = true)
    {
        if (Mounted)
        {
            return (dir & Direction.Running) != 0 ? CalcMoves.RunMountDelay : CalcMoves.WalkMountDelay;
        }

        return (dir & Direction.Running) != 0 ? CalcMoves.RunFootDelay : CalcMoves.WalkFootDelay;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when a Mobile <paramref name="m" /> moves off this Mobile.
    /// </summary>
    /// <returns>True if the move is allowed, false if not.</returns>
    public virtual bool OnMoveOff(Mobile m) => true;

    /// <summary>
    ///     Overridable. Event invoked when a Mobile <paramref name="m" /> moves over this Mobile.
    /// </summary>
    /// <returns>True if the move is allowed, false if not.</returns>
    public virtual bool OnMoveOver(Mobile m) => m_Map == null || Deleted || m.CheckShove(this);

    public virtual bool CheckShove(Mobile shoved)
    {
        if ((m_Map.Rules & MapRules.FreeMovement) == 0)
        {
            if (!shoved.Alive || !Alive || shoved.IsDeadBondedPet || IsDeadBondedPet)
            {
                return true;
            }

            if (shoved.m_Hidden && shoved.m_AccessLevel > AccessLevel.Player)
            {
                return true;
            }

            if (!Pushing)
            {
                Pushing = true;

                int number;

                if (AccessLevel > AccessLevel.Player)
                {
                    number = shoved.m_Hidden ? 1019041 : 1019040;
                }
                else
                {
                    if (Stam == StamMax)
                    {
                        number = shoved.m_Hidden ? 1019043 : 1019042;
                        Stam -= 10;

                        RevealingAction();
                    }
                    else
                    {
                        return false;
                    }
                }

                SendLocalizedMessage(number);
            }
        }

        return true;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile sees another Mobile, <paramref name="m" />, move.
    /// </summary>
    public virtual void OnMovement(Mobile m, Point3D oldLocation)
    {
    }

    public virtual void CriminalAction(bool message)
    {
        if (Deleted)
        {
            return;
        }

        Criminal = true;

        Region.OnCriminalAction(this, message);
    }

    public virtual bool IsSnoop(Mobile from) => from != this;

    /// <summary>
    ///     Overridable. Any call to <see cref="Resurrect" /> will silently fail if this method returns false.
    ///     <seealso cref="Resurrect" />
    /// </summary>
    public virtual bool CheckResurrect() => true;

    /// <summary>
    ///     Overridable. Event invoked before the Mobile is <see cref="Resurrect">resurrected</see>.
    ///     <seealso cref="Resurrect" />
    /// </summary>
    public virtual void OnBeforeResurrect()
    {
    }

    /// <summary>
    ///     Overridable. Event invoked after the Mobile is <see cref="Resurrect">resurrected</see>.
    ///     <seealso cref="Resurrect" />
    /// </summary>
    public virtual void OnAfterResurrect()
    {
    }

    public virtual void Resurrect()
    {
        if (!Alive)
        {
            if (!Region.OnResurrect(this))
            {
                return;
            }

            if (!CheckResurrect())
            {
                return;
            }

            OnBeforeResurrect();

            var box = FindBankNoCreate();

            if (box?.Opened == true)
            {
                box.Close();
            }

            Poison = null;

            Warmode = false;

            Hits = 10;
            Stam = StamMax;
            Mana = 0;

            BodyMod = 0;
            Body = Race.AliveBody(this);

            ProcessDelta();

            for (var i = Items.Count - 1; i >= 0; --i)
            {
                if (i >= Items.Count)
                {
                    continue;
                }

                var item = Items[i];

                if (item.ItemID == 0x204E)
                {
                    item.Delete();
                }
            }

            SendIncomingPacket();

            OnAfterResurrect();
        }
    }

    public void DropHolding()
    {
        var holding = m_Holding;

        if (holding != null)
        {
            if (!holding.Deleted && holding.HeldBy == this && holding.Map == Map.Internal)
            {
                AddToBackpack(holding);
            }

            Holding = null;
            holding.ClearBounce();
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked before the Mobile is deleted.
    /// </summary>
    public virtual void OnDelete()
    {
        Spawner?.Remove(this);
        Spawner = null;
    }

    public virtual bool CheckSpellCast(ISpell spell) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile casts a <paramref name="spell" />.
    /// </summary>
    /// <param name="spell"></param>
    public virtual void OnSpellCast(ISpell spell)
    {
        var now = Core.TickCount;
        if (m_NetState != null && now - m_NetState._nextMovementTime > 0)
        {
            m_NetState._nextMovementTime = now;
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked after <see cref="TotalWeight" /> changes.
    /// </summary>
    public virtual void OnWeightChange(int oldValue)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the <see cref="Skill.Base" /> or <see cref="Skill.BaseFixedPoint" /> property of
    ///     <paramref name="skill" /> changes.
    /// </summary>
    public virtual void OnSkillChange(SkillName skill, double oldBase)
    {
    }

    /// <summary>
    ///     Overridable. Invoked after the mobile is deleted. When overridden, be sure to call the base method.
    /// </summary>
    public virtual void OnAfterDelete()
    {
        StopAggrExpire();

        CheckAggrExpire();

        PoisonTimer?.Stop();
        m_HitsTimer?.Stop();
        m_StamTimer?.Stop();
        m_ManaTimer?.Stop();
        _combatTimerToken.Cancel();
        _expireCombatantTimerToken.Cancel();
        _logoutTimerToken.Cancel();
        _expireCriminalTimerToken.Cancel();
        _paraTimerToken.Cancel();
        _frozenTimerToken.Cancel();
        _autoManifestTimerToken.Cancel();
    }

    public virtual bool AllowSkillUse(SkillName name) => true;

    public virtual bool UseSkill(SkillName name) => Skills.UseSkill(this, name);

    public virtual bool UseSkill(int skillID) => Skills.UseSkill(this, skillID);

    public virtual DeathMoveResult GetParentMoveResultFor(Item item) => item.OnParentDeath(this);

    public virtual DeathMoveResult GetInventoryMoveResultFor(Item item) => item.OnInventoryDeath(this);

    public virtual void Kill()
    {
        if (!CanBeDamaged())
        {
            return;
        }

        if (!Alive || IsDeadBondedPet)
        {
            return;
        }

        if (Deleted)
        {
            return;
        }

        if (!Region.OnBeforeDeath(this))
        {
            return;
        }

        if (!OnBeforeDeath())
        {
            return;
        }

        var box = FindBankNoCreate();

        if (box?.Opened == true)
        {
            box.Close();
        }

        m_NetState?.CancelAllTrades();

        m_Spell?.OnCasterKilled();
        // m_Spell.Disturb( DisturbType.Kill );

        m_Target?.Cancel(this, TargetCancelType.Canceled);

        DisruptiveAction();

        Warmode = false;

        DropHolding();

        Hits = 0;
        Stam = 0;
        Mana = 0;

        Poison = null;
        Combatant = null;

        if (Paralyzed)
        {
            Paralyzed = false;
        }

        if (Frozen)
        {
            Frozen = false;
        }

        var content = new List<Item>();
        var equip = new List<Item>();
        var moveToPack = new List<Item>();

        var itemsCopy = new List<Item>(Items);

        var pack = Backpack;

        for (var i = 0; i < itemsCopy.Count; ++i)
        {
            var item = itemsCopy[i];

            if (item == pack)
            {
                continue;
            }

            var res = GetParentMoveResultFor(item);

            switch (res)
            {
                case DeathMoveResult.MoveToCorpse:
                    {
                        content.Add(item);
                        equip.Add(item);
                        break;
                    }
                case DeathMoveResult.MoveToBackpack:
                    {
                        moveToPack.Add(item);
                        break;
                    }
            }
        }

        if (pack != null)
        {
            var packCopy = new List<Item>(pack.Items);

            for (var i = 0; i < packCopy.Count; ++i)
            {
                var item = packCopy[i];

                var res = GetInventoryMoveResultFor(item);

                if (res == DeathMoveResult.MoveToCorpse)
                {
                    content.Add(item);
                }
                else
                {
                    moveToPack.Add(item);
                }
            }

            for (var i = 0; i < moveToPack.Count; ++i)
            {
                var item = moveToPack[i];

                if (RetainPackLocsOnDeath && item.Parent == pack)
                {
                    continue;
                }

                pack.DropItem(item);
            }
        }

        var c = CreateCorpseHandler?.Invoke(this, content, equip);

        if (m_Map != null)
        {
            var corpseSerial = c?.Serial ?? Serial.Zero;

            Span<byte> deathAnimation = stackalloc byte[OutgoingMobilePackets.DeathAnimationPacketLength].InitializePacket();
            Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();

            foreach (var state in m_Map.GetClientsInRange(m_Location))
            {
                if (state != m_NetState)
                {
                    OutgoingMobilePackets.CreateDeathAnimation(deathAnimation, Serial, corpseSerial);
                    state.Send(deathAnimation);

                    if (!state.Mobile.CanSee(this))
                    {
                        OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
                        state.Send(removeEntity);
                    }
                }
            }
        }

        Region.OnDeath(this);
        OnDeath(c);
    }

    /// <summary>
    ///     Overridable. Event invoked before the Mobile is <see cref="Kill">killed</see>.
    ///     <seealso cref="Kill" />
    ///     <seealso cref="OnDeath" />
    /// </summary>
    /// <returns>True to continue with death, false to override it.</returns>
    public virtual bool OnBeforeDeath() => true;

    /// <summary>
    ///     Overridable. Event invoked after the Mobile is <see cref="Kill">killed</see>. Primarily, this method is responsible for
    ///     deleting an NPC or turning a PC into a ghost.
    ///     <seealso cref="Kill" />
    ///     <seealso cref="OnBeforeDeath" />
    /// </summary>
    public virtual void OnDeath(Container c)
    {
        var sound = GetDeathSound();

        if (sound >= 0)
        {
            Effects.PlaySound(this, sound);
        }

        if (!m_Player)
        {
            Delete();
        }
        else
        {
            m_NetState.SendDeathStatus();

            Warmode = false;

            BodyMod = 0;
            // Body = this.Female ? 0x193 : 0x192;
            Body = Race.GhostBody(this);

            var deathShroud = new Item(0x204E) { Movable = false, Layer = Layer.OuterTorso };

            AddItem(deathShroud);

            Items.Remove(deathShroud);
            Items.Insert(0, deathShroud);

            Poison = null;
            Combatant = null;

            Hits = 0;
            Stam = 0;
            Mana = 0;

            EventSink.InvokePlayerDeath(this);

            ProcessDelta();

            CheckStatTimers();
        }
    }

    public virtual bool CheckTarget(Mobile from, Target targ, object targeted) => true;

    public virtual void Use(Item item)
    {
        if (item?.Deleted != false || item.QuestItem || Deleted)
        {
            return;
        }

        DisruptiveAction();

        if (m_Spell?.OnCasterUsingObject(item) == false)
        {
            return;
        }

        var root = item.RootParent;
        var okay = false;

        if (!Utility.InUpdateRange(Location, item.GetWorldLocation()))
        {
            item.OnDoubleClickOutOfRange(this);
        }
        else if (!CanSee(item))
        {
            item.OnDoubleClickCantSee(this);
        }
        else if (!item.IsAccessibleTo(this))
        {
            var reg = Region.Find(item.GetWorldLocation(), item.Map);

            if (reg?.SendInaccessibleMessage(item, this) != true)
            {
                item.OnDoubleClickNotAccessible(this);
            }
        }
        else if (!CheckAlive(false))
        {
            item.OnDoubleClickDead(this);
        }
        else if (item.InSecureTrade)
        {
            item.OnDoubleClickSecureTrade(this);
        }
        else if (!AllowItemUse(item))
        {
        }
        else if (!item.CheckItemUse(this, item))
        {
        }
        else if (root is Mobile mobile && mobile.IsSnoop(this))
        {
            item.OnSnoop(this);
        }
        else if (Region.OnDoubleClick(this, item))
        {
            okay = true;
        }

        if (okay)
        {
            // TODO: Is this correct?
            if (!item.Deleted)
            {
                item.OnItemUsed(this, item);
            }

            // TODO: Is this correct?
            if (!item.Deleted)
            {
                item.OnDoubleClick(this);
            }
        }
    }

    public virtual void Use(Mobile m)
    {
        if (m?.Deleted != false || Deleted)
        {
            return;
        }

        DisruptiveAction();

        if (m_Spell?.OnCasterUsingObject(m) == false)
        {
            return;
        }

        if (!Utility.InUpdateRange(Location, m.Location))
        {
            m.OnDoubleClickOutOfRange(this);
        }
        else if (!CanSee(m))
        {
            m.OnDoubleClickCantSee(this);
        }
        else if (!CheckAlive(false))
        {
            m.OnDoubleClickDead(this);
        }
        else if (Region.OnDoubleClick(this, m) && !m.Deleted)
        {
            m.OnDoubleClick(this);
        }
    }

    public virtual void Lift(Item item, int amount, out bool rejected, out LRReason reject)
    {
        rejected = true;
        reject = LRReason.Inspecific;

        if (item == null)
        {
            return;
        }

        var from = this;
        var state = m_NetState;

        if (from.AccessLevel >= AccessLevel.GameMaster || Core.TickCount - from.NextActionTime >= 0)
        {
            if (from.CheckAlive())
            {
                from.DisruptiveAction();

                if (from.Holding != null)
                {
                    reject = LRReason.AreHolding;
                }
                else if (from.AccessLevel < AccessLevel.GameMaster && !from.InRange(item.GetWorldLocation(), 2))
                {
                    reject = LRReason.OutOfRange;
                }
                else if (!from.CanSee(item) || !from.InLOS(item))
                {
                    reject = LRReason.OutOfSight;
                }
                else if (!item.VerifyMove(from))
                {
                    reject = LRReason.CannotLift;
                }
                else if (!item.IsAccessibleTo(from))
                {
                    reject = LRReason.CannotLift;
                }
                else if (item.Nontransferable && amount != item.Amount)
                {
                    if (item.QuestItem)
                    {
                        from.SendLocalizedMessage(1074868); // Stacks of quest items cannot be unstacked.
                    }

                    reject = LRReason.CannotLift;
                }
                else if (!item.CheckLift(from, item, ref reject))
                {
                }
                else
                {
                    var root = item.RootParent;

                    if (root is Mobile mobile && !mobile.CheckNonlocalLift(from, item))
                    {
                        reject = LRReason.TryToSteal;
                    }
                    else if (!from.OnDragLift(item) || !item.OnDragLift(from))
                    {
                        reject = LRReason.Inspecific;
                    }
                    else if (!from.CheckAlive())
                    {
                        reject = LRReason.Inspecific;
                    }
                    else
                    {
                        item.SetLastMoved();

                        if (item.Spawner != null)
                        {
                            item.Spawner.Remove(item);
                            item.Spawner = null;
                        }

                        var oldAmount = item.Amount;

                        if (oldAmount <= 0)
                        {
                            logger.Error(
                                "Item {Type} ({Serial}) has amount of {OldAmount}, but must be at least 1",
                                item.GetType(),
                                item.Serial,
                                oldAmount
                            );
                        }
                        else
                        {
                            amount = Math.Clamp(amount, 1, oldAmount);

                            if (amount < oldAmount)
                            {
                                LiftItemDupe(item, amount);
                            }
                        }

                        var map = from.Map;

                        if (DragEffects && map != null && root is null or Item)
                        {
                            var rootItem = root as Item;

                            Span<byte> buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength].InitializePacket();

                            foreach (var ns in map.GetClientsInRange(from.Location))
                            {
                                if (ns.Mobile != from && ns.Mobile.CanSee(from) && ns.Mobile.InLOS(from) &&
                                    ns.Mobile.CanSee(root))
                                {
                                    OutgoingPlayerPackets.CreateDragEffect(
                                        buffer,
                                        rootItem?.Serial ?? Serial.Zero,
                                        rootItem?.Location ?? item.Location,
                                        from.Serial,
                                        from.Location,
                                        item.ItemID,
                                        item.Hue,
                                        amount
                                    );

                                    ns.Send(buffer);
                                }
                            }
                        }

                        var fixLoc = item.Location;
                        var fixMap = item.Map;
                        var shouldFix = item.Parent == null;

                        item.RecordBounce();
                        item.OnItemLifted(from, item);
                        item.Internalize();

                        from.Holding = item;

                        from.SendSound(item.GetLiftSound(from));

                        from.NextActionTime = Core.TickCount + ActionDelay;

                        if (fixMap != null && shouldFix)
                        {
                            fixMap.FixColumn(fixLoc.m_X, fixLoc.m_Y);
                        }

                        reject = LRReason.Inspecific;
                        rejected = false;
                    }
                }
            }
            else
            {
                reject = LRReason.Inspecific;
            }
        }
        else
        {
            SendActionMessage();
            reject = LRReason.Inspecific;
        }

        if (rejected && state != null)
        {
            state.SendLiftReject(reject);

            if (item.Deleted)
            {
                return;
            }

            if (item.Parent is Item)
            {
                state.SendContainerContentUpdate(item);
            }
            else if (item.Parent is Mobile)
            {
                state.SendEquipUpdate(item);
            }
            else
            {
                item.SendInfoTo(state);
            }

            if (item.Parent != null)
            {
                item.SendOPLPacketTo(state);
            }
        }
    }

    public static T LiftItemDupe<T>(T oldItem, int amount) where T : Item
    {
        T item;
        try
        {
            item = oldItem.GetType().CreateInstance<T>();
        }
        catch (Exception e)
        {
            logger.Warning(
                e,
                "[{Serial} {Name}]: Item must have a zero parameter constructor to be separated from a stack.",
                oldItem.Serial,
                oldItem.GetType().Name
            );

            return null;
        }

        var oldAmount = oldItem.Amount;
        oldItem.Amount = amount;

        item.Visible = oldItem.Visible;
        item.Movable = oldItem.Movable;
        item.LootType = oldItem.LootType;
        item.Direction = oldItem.Direction;
        item.Hue = oldItem.Hue;
        item.ItemID = oldItem.ItemID;
        item.Location = oldItem.Location;
        item.Layer = oldItem.Layer;
        item.Name = oldItem.Name;
        item.Weight = oldItem.Weight;

        item.Amount = oldAmount - amount;
        item.Map = oldItem.Map;

        oldItem.OnAfterDuped(item);

        if (oldItem.Parent is Mobile parentMobile)
        {
            parentMobile.AddItem(item);
        }
        else if (oldItem.Parent is Item parentItem)
        {
            parentItem.AddItem(item);
        }

        item.Delta(ItemDelta.Update);

        return item;
    }

    public virtual void SendDropEffect(Item item)
    {
        if (!DragEffects || item.Deleted)
        {
            return;
        }

        var map = m_Map;
        var root = item.RootParent;
        var rootItem = root as Item;

        if (map == null || root != null && rootItem == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingPlayerPackets.DragEffectPacketLength].InitializePacket();

        foreach (var ns in map.GetClientsInRange(m_Location))
        {
            if (ns.StygianAbyss || ns.Mobile == this ||
                !ns.Mobile.CanSee(this) || !ns.Mobile.InLOS(this) || !ns.Mobile.CanSee(root))
            {
                continue;
            }

            OutgoingPlayerPackets.CreateDragEffect(
                buffer,
                Serial,
                Location,
                rootItem?.Serial ?? Serial.Zero,
                rootItem?.Location ?? item.Location,
                item.ItemID,
                item.Hue,
                item.Amount
            );

            ns.Send(buffer);
        }
    }

    public virtual bool Drop(Item to, Point3D loc)
    {
        var from = this;
        var item = from.Holding;

        var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;

        from.Holding = null;

        if (!valid)
        {
            return false;
        }

        var bounced = true;

        item.SetLastMoved();

        if (to == null || !item.DropToItem(from, to, loc))
        {
            item.Bounce(from);
        }
        else
        {
            bounced = false;
        }

        item.ClearBounce();

        if (!bounced)
        {
            SendDropEffect(item);
        }

        return !bounced;
    }

    public virtual bool Drop(Point3D loc)
    {
        var from = this;
        var item = from.Holding;

        var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;

        from.Holding = null;

        if (!valid)
        {
            return false;
        }

        var bounced = true;

        item.SetLastMoved();

        if (!item.DropToWorld(from, loc))
        {
            item.Bounce(from);
        }
        else
        {
            bounced = false;
        }

        item.ClearBounce();

        if (!bounced)
        {
            SendDropEffect(item);
        }

        return !bounced;
    }

    public virtual bool Drop(Mobile to, Point3D loc)
    {
        var from = this;
        var item = from.Holding;

        var valid = item != null && item.HeldBy == from && item.Map == Map.Internal;

        from.Holding = null;

        if (!valid)
        {
            return false;
        }

        var bounced = true;

        item.SetLastMoved();

        if (to == null || !item.DropToMobile(from, to, loc))
        {
            item.Bounce(from);
        }
        else
        {
            bounced = false;
        }

        item.ClearBounce();

        if (!bounced)
        {
            SendDropEffect(item);
        }

        return !bounced;
    }

    public virtual bool MutateSpeech(List<Mobile> hears, ref string text, ref object context)
    {
        if (Alive)
        {
            return false;
        }

        using var sb = new ValueStringBuilder(stackalloc char[Math.Min(text.Length, 256)]);
        for (var i = 0; i < text.Length; ++i)
        {
            sb.Append(text[i] != ' ' ? (GhostChars ?? DefaultGhostChars).RandomElement() : ' ');
        }

        text = sb.ToString();
        context = m_GhostMutateContext;
        return true;
    }

    private void AutoManifest()
    {
        if (!Alive)
        {
            Warmode = false;
        }
    }

    public virtual void Manifest(TimeSpan delay)
    {
        Warmode = true;
        _autoManifestTimerToken.Cancel();
        Timer.StartTimer(delay, AutoManifest, out _autoManifestTimerToken);
    }

    public virtual bool CheckSpeechManifest()
    {
        if (Alive)
        {
            return false;
        }

        var delay = AutoManifestTimeout;

        if (delay > TimeSpan.Zero && (!Warmode || _autoManifestTimerToken.Running))
        {
            Manifest(delay);
            return true;
        }

        return false;
    }

    public virtual bool CheckHearsMutatedSpeech(Mobile m, object context) =>
        context != m_GhostMutateContext || m.Alive && !m.CanHearGhosts;

    private void AddSpeechItemsFrom(List<IEntity> list, Container cont)
    {
        for (var i = 0; i < cont.Items.Count; ++i)
        {
            var item = cont.Items[i];

            if (item.HandlesOnSpeech)
            {
                list.Add(item);
            }

            if (item is Container container)
            {
                AddSpeechItemsFrom(list, container);
            }
        }
    }

    public virtual void DoSpeech(string text, int[] keywords, MessageType type, int hue)
    {
        if (Deleted || CommandSystem.Handle(this, text, type))
        {
            return;
        }

        var range = 15;

        switch (type)
        {
            case MessageType.Regular:
                {
                    SpeechHue = hue;
                    break;
                }
            case MessageType.Emote:
                {
                    EmoteHue = hue;
                    break;
                }
            case MessageType.Whisper:
                {
                    WhisperHue = hue;
                    range = 1;
                    break;
                }
            case MessageType.Yell:
                {
                    YellHue = hue;
                    range = 18;
                    break;
                }
            case MessageType.System:
                {
                    break;
                }
            case MessageType.Label:
                {
                    break;
                }
            case MessageType.Focus:
                {
                    break;
                }
            case MessageType.Spell:
                {
                    break;
                }
            case MessageType.Guild:
                {
                    break;
                }
            case MessageType.Alliance:
                {
                    break;
                }
            case MessageType.Command:
                {
                    break;
                }
            case MessageType.Encoded:
                {
                    break;
                }
            default:
                {
                    type = MessageType.Regular;
                    break;
                }
        }

        var regArgs = new SpeechEventArgs(this, text, type, hue, keywords);

        EventSink.InvokeSpeech(regArgs);
        Region.OnSpeech(regArgs);
        OnSaid(regArgs);

        if (regArgs.Blocked)
        {
            return;
        }

        text = regArgs.Speech;

        if (string.IsNullOrEmpty(text))
        {
            return;
        }

        var hears = m_Hears;
        var onSpeech = m_OnSpeech;

        if (m_Map != null)
        {
            foreach (var heard in m_Map.GetMobilesInRange(m_Location, range))
            {
                if (!heard.CanSee(this) || !NoSpeechLOS && heard.Player && !heard.InLOS(this))
                {
                    continue;
                }

                if (heard.m_NetState != null)
                {
                    hears.Add(heard);
                }

                if (heard.HandlesOnSpeech(this))
                {
                    onSpeech.Add(heard);
                }

                for (var i = 0; i < heard.Items.Count; ++i)
                {
                    var item = heard.Items[i];

                    if (item.HandlesOnSpeech)
                    {
                        onSpeech.Add(item);
                    }

                    if (item is Container container)
                    {
                        AddSpeechItemsFrom(onSpeech, container);
                    }
                }
            }

            foreach (var item in m_Map.GetItemsInRange(m_Location, range))
            {
                if (item.HandlesOnSpeech)
                {
                    onSpeech.Add(item);
                }

                if (item is Container container)
                {
                    AddSpeechItemsFrom(onSpeech, container);
                }
            }

            object mutateContext = null;
            var mutatedText = text;
            SpeechEventArgs mutatedArgs = null;

            if (MutateSpeech(hears, ref mutatedText, ref mutateContext))
            {
                mutatedArgs = new SpeechEventArgs(this, mutatedText, type, hue, Array.Empty<int>());
            }

            CheckSpeechManifest();

            ProcessDelta();

            Span<byte> regBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();
            Span<byte> mutBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(mutatedText)].InitializePacket();

            // TODO: Should this be sorted like onSpeech is below?
            for (var i = 0; i < hears.Count; ++i)
            {
                var heard = hears[i];

                if (mutatedArgs == null || !CheckHearsMutatedSpeech(heard, mutateContext))
                {
                    var length = OutgoingMessagePackets.CreateMessage(
                        regBuffer, Serial, Body, type, hue, 3, false, m_Language, Name, text
                    );

                    if (length != regBuffer.Length)
                    {
                        regBuffer = regBuffer[..length]; // Adjust to the actual size
                    }

                    heard.OnSpeech(regArgs);
                    heard.NetState?.Send(regBuffer);
                }
                else
                {
                    var length = OutgoingMessagePackets.CreateMessage(
                        mutBuffer, Serial, Body, type, hue, 3, false, m_Language, Name, mutatedText
                    );

                    if (length != mutBuffer.Length)
                    {
                        mutBuffer = mutBuffer[..length]; // Adjust to the actual size
                    }

                    heard.OnSpeech(mutatedArgs);
                    heard.NetState?.Send(mutBuffer);
                }
            }

            if (onSpeech.Count > 1)
            {
                onSpeech.Sort(LocationComparer.GetInstance(this));
            }

            for (var i = 0; i < onSpeech.Count; ++i)
            {
                var obj = onSpeech[i];

                if (obj is Mobile heard)
                {
                    if (mutatedArgs == null || !CheckHearsMutatedSpeech(heard, mutateContext))
                    {
                        heard.OnSpeech(regArgs);
                    }
                    else
                    {
                        heard.OnSpeech(mutatedArgs);
                    }
                }
                else
                {
                    ((Item)obj).OnSpeech(regArgs);
                }
            }

            if (m_Hears.Count > 0)
            {
                m_Hears.Clear();
            }

            if (m_OnSpeech.Count > 0)
            {
                m_OnSpeech.Clear();
            }
        }
    }

    public Mobile FindMostRecentDamager(bool allowSelf) => FindMostRecentDamageEntry(allowSelf)?.Damager;

    public DamageEntry FindMostRecentDamageEntry(bool allowSelf)
    {
        for (var i = DamageEntries.Count - 1; i >= 0; --i)
        {
            if (i >= DamageEntries.Count)
            {
                continue;
            }

            var de = DamageEntries[i];

            if (de.HasExpired)
            {
                DamageEntries.RemoveAt(i);
            }
            else if (allowSelf || de.Damager != this)
            {
                return de;
            }
        }

        return null;
    }

    public Mobile FindLeastRecentDamager(bool allowSelf) => FindLeastRecentDamageEntry(allowSelf)?.Damager;

    public DamageEntry FindLeastRecentDamageEntry(bool allowSelf)
    {
        for (var i = 0; i < DamageEntries.Count; ++i)
        {
            if (i < 0)
            {
                continue;
            }

            var de = DamageEntries[i];

            if (de.HasExpired)
            {
                DamageEntries.RemoveAt(i);
                --i;
            }
            else if (allowSelf || de.Damager != this)
            {
                return de;
            }
        }

        return null;
    }

    public Mobile FindMostTotalDamager(bool allowSelf) => FindMostTotalDamageEntry(allowSelf)?.Damager;

    public DamageEntry FindMostTotalDamageEntry(bool allowSelf)
    {
        DamageEntry mostTotal = null;

        for (var i = DamageEntries.Count - 1; i >= 0; --i)
        {
            if (i >= DamageEntries.Count)
            {
                continue;
            }

            var de = DamageEntries[i];

            if (de.HasExpired)
            {
                DamageEntries.RemoveAt(i);
            }
            else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven > mostTotal.DamageGiven))
            {
                mostTotal = de;
            }
        }

        return mostTotal;
    }

    public Mobile FindLeastTotalDamager(bool allowSelf) => FindLeastTotalDamageEntry(allowSelf)?.Damager;

    public DamageEntry FindLeastTotalDamageEntry(bool allowSelf)
    {
        DamageEntry mostTotal = null;

        for (var i = DamageEntries.Count - 1; i >= 0; --i)
        {
            if (i >= DamageEntries.Count)
            {
                continue;
            }

            var de = DamageEntries[i];

            if (de.HasExpired)
            {
                DamageEntries.RemoveAt(i);
            }
            else if ((allowSelf || de.Damager != this) && (mostTotal == null || de.DamageGiven < mostTotal.DamageGiven))
            {
                mostTotal = de;
            }
        }

        return mostTotal;
    }

    public DamageEntry FindDamageEntryFor(Mobile m)
    {
        for (var i = DamageEntries.Count - 1; i >= 0; --i)
        {
            if (i >= DamageEntries.Count)
            {
                continue;
            }

            var de = DamageEntries[i];

            if (de.HasExpired)
            {
                DamageEntries.RemoveAt(i);
            }
            else if (de.Damager == m)
            {
                return de;
            }
        }

        return null;
    }

    public virtual Mobile GetDamageMaster(Mobile damagee) => null;

    public virtual DamageEntry RegisterDamage(int amount, Mobile from)
    {
        var de = FindDamageEntryFor(from) ?? new DamageEntry(from);

        de.DamageGiven += amount;
        de.LastDamage = Core.Now;

        DamageEntries.Remove(de);
        DamageEntries.Add(de);

        var master = from.GetDamageMaster(this);

        if (master != null)
        {
            var list = de.Responsible;

            if (list == null)
            {
                de.Responsible = list = new List<DamageEntry>();
            }

            DamageEntry resp = null;
            foreach (var check in list)
            {
                if (check.Damager == master)
                {
                    resp = check;
                    break;
                }
            }

            if (resp == null)
            {
                list.Add(resp = new DamageEntry(master));
            }

            resp.DamageGiven += amount;
            resp.LastDamage = Core.Now;
        }

        return de;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile is <see cref="Damage">damaged</see>. It is called before
    ///     <see cref="Hits">hit points</see> are lowered or the Mobile is <see cref="Kill">killed</see>.
    ///     <seealso cref="Damage" />
    ///     <seealso cref="Hits" />
    ///     <seealso cref="Kill" />
    /// </summary>
    public virtual void OnDamage(int amount, Mobile from, bool willKill)
    {
    }

    public virtual bool CanBeDamaged() => !m_Blessed;

    public virtual void Damage(int amount, Mobile from = null, bool informMount = true, bool ignoreEvilOmen = false)
    {
        if (amount <= 0)
        {
            return;
        }

        if (!CanBeDamaged() || Deleted)
        {
            return;
        }

        if (!Region.OnDamage(this, ref amount))
        {
            return;
        }

        var oldHits = Hits;
        var newHits = oldHits - amount;

        m_Spell?.OnCasterHurt();

        // if (m_Spell != null && m_Spell.State == SpellState.Casting)
        // m_Spell.Disturb( DisturbType.Hurt, false, true );

        if (from != null)
        {
            RegisterDamage(amount, from);
        }

        DisruptiveAction();

        Paralyzed = false;

        switch (VisibleDamageType)
        {
            case VisibleDamageType.Related:
                {
                    SendVisibleDamageRelated(from, amount);
                    break;
                }
            case VisibleDamageType.Everyone:
                {
                    SendVisibleDamageEveryone(amount);
                    break;
                }
            case VisibleDamageType.Selective:
                {
                    SendVisibleDamageSelective(from, amount);
                    break;
                }
        }

        OnDamage(amount, from, newHits < 0);

        if (informMount)
        {
            Mount?.OnRiderDamaged(amount, from, newHits < 0);
        }

        if (newHits < 0)
        {
            LastKiller = from;

            Hits = 0;

            if (oldHits >= 0)
            {
                Kill();
            }
        }
        else
        {
            Hits = newHits;
        }
    }

    public void SendVisibleDamageRelated(Mobile from, int amount)
    {
        var ourState = m_NetState ?? GetDamageMaster(from)?.m_NetState;
        var theirState = from?.m_NetState ?? from?.GetDamageMaster(this)?.m_NetState;

        if (amount > 0)
        {
            ourState.SendDamage(Serial, amount);

            if (theirState != null && theirState != ourState)
            {
                theirState.SendDamage(Serial, amount);
            }
        }
    }

    public void SendVisibleDamageEveryone(int amount)
    {
        if (amount < 0)
        {
            return;
        }

        var map = m_Map;

        if (map == null)
        {
            return;
        }

        foreach (var ns in map.GetClientsInRange(m_Location))
        {
            if (ns.Mobile.CanSee(this))
            {
                ns.SendDamage(Serial, amount);
            }
        }
    }

    public void SendVisibleDamageSelective(Mobile from, int amount)
    {
        var ourState = m_NetState;
        var theirState = from?.m_NetState;

        var damager = from;
        var damaged = this;

        if (ourState == null)
        {
            var master = GetDamageMaster(from);

            if (master != null)
            {
                damaged = master;
                ourState = master.m_NetState;
            }
        }

        if (!damaged.ShowVisibleDamage)
        {
            return;
        }

        if (theirState == null && from != null)
        {
            var master = from.GetDamageMaster(this);

            if (master != null)
            {
                damager = master;
                theirState = master.m_NetState;
            }
        }

        if (amount > 0)
        {
            if (damaged.CanSeeVisibleDamage)
            {
                ourState.SendDamage(Serial, amount);
            }

            if (theirState != null && theirState != ourState && damager.CanSeeVisibleDamage)
            {
                theirState.SendDamage(Serial, amount);
            }
        }
    }

    public void Heal(int amount) => Heal(amount, this);

    public void Heal(int amount, Mobile from, bool message = true)
    {
        if (!Alive || IsDeadBondedPet)
        {
            return;
        }

        if (!Region.OnHeal(this, ref amount))
        {
            return;
        }

        OnHeal(ref amount, from);

        if (Hits + amount > HitsMax)
        {
            amount = HitsMax - Hits;
        }

        Hits += amount;

        if (message && amount > 0)
        {
            m_NetState.SendMessageLocalizedAffix(
                Serial.MinusOne,
                -1,
                MessageType.Label,
                0x3B2,
                3,
                1008158,
                "",
                AffixType.Append | AffixType.System,
                amount.ToString()
            );
        }
    }

    public virtual void OnHeal(ref int amount, Mobile from)
    {
    }

    public virtual void Deserialize(IGenericReader reader)
    {
        var version = reader.ReadInt();

        switch (version)
        {
            case 36: // Moved virtues to VirtueSystem
            case 35: // Moved short term murders to PlayerMurderSystem
            case 34: // Moved Stabled to PlayerMobile
            case 33: // Removed created
            case 32: // Removed StuckMenu
            case 31:
                {
                    LastStrGain = reader.ReadDeltaTime();
                    LastIntGain = reader.ReadDeltaTime();
                    LastDexGain = reader.ReadDeltaTime();

                    goto case 30;
                }
            case 30:
                {
                    var hairflag = reader.ReadByte();

                    if ((hairflag & 0x01) != 0)
                    {
                        _hair = new VirtualHairInfo();
                        _hair.Deserialize(reader);
                    }

                    if ((hairflag & 0x02) != 0)
                    {
                        _facialHair = new VirtualHairInfo();
                        _facialHair.Deserialize(reader);
                    }

                    goto case 29;
                }
            case 29:
                {
                    m_Race = reader.ReadRace();
                    goto case 28;
                }
            case 28:
                {
                    if (version <= 30)
                    {
                        LastStatGain = reader.ReadDeltaTime();
                    }

                    goto case 27;
                }
            case 27:
                {
                    m_TithingPoints = reader.ReadInt();

                    goto case 26;
                }
            case 26:
            case 25:
            case 24:
                {
                    Corpse = reader.ReadEntity<Container>();

                    goto case 23;
                }
            case 23:
                {
                    if (version < 33)
                    {
                        Created = reader.ReadDateTime();
                    }

                    goto case 22;
                }
            case 22: // Just removed followers
            case 21:
                {
                    if (version < 34)
                    {
                        // Migrated to PlayerMobile
                        AddToStabledMigration(this, reader.ReadEntitySet<Mobile>(true));
                    }

                    goto case 20;
                }
            case 20:
                {
                    CantWalk = reader.ReadBool();

                    goto case 19;
                }
            case 19: // Just removed variables
            case 18:
                {
                    if (version < 36)
                    {
                        reader.ReadByte(); // VirtueInfo version

                        int mask = reader.ReadByte();

                        if (mask != 0)
                        {
                            var virtueValues = new int[8];

                            for (var i = 0; i < 8; ++i)
                            {
                                if ((mask & (1 << i)) != 0)
                                {
                                    virtueValues[i] = reader.ReadInt();
                                }
                            }

                            AddToVirtueMigration(this, virtueValues);
                        }
                    }

                    goto case 17;
                }
            case 17:
                {
                    Thirst = reader.ReadInt();
                    BAC = reader.ReadInt();

                    goto case 16;
                }
            case 16:
                {
                    if (version < 35)
                    {
                        // Migrated to PlayerMurderSystem
                        AddToMurderMigration(this, reader.ReadInt());
                    }

                    if (version <= 24)
                    {
                        reader.ReadDateTime();
                        reader.ReadDateTime();
                    }

                    goto case 15;
                }
            case 15:
                {
                    if (version < 22)
                    {
                        reader.ReadInt(); // followers
                    }

                    m_FollowersMax = reader.ReadInt();

                    goto case 14;
                }
            case 14:
                {
                    MagicDamageAbsorb = reader.ReadInt();

                    goto case 13;
                }
            case 13:
                {
                    GuildFealty = reader.ReadEntity<Mobile>();

                    goto case 12;
                }
            case 12:
                {
                    m_Guild = reader.ReadEntity<BaseGuild>();

                    goto case 11;
                }
            case 11:
                {
                    m_DisplayGuildTitle = reader.ReadBool();

                    goto case 10;
                }
            case 10:
                {
                    CanSwim = reader.ReadBool();

                    goto case 9;
                }
            case 9:
                {
                    Squelched = reader.ReadBool();

                    goto case 8;
                }
            case 8:
                {
                    m_Holding = reader.ReadEntity<Item>();

                    goto case 7;
                }
            case 7:
                {
                    m_VirtualArmor = reader.ReadInt();

                    goto case 6;
                }
            case 6:
                {
                    BaseSoundID = reader.ReadInt();

                    goto case 5;
                }
            case 5:
                {
                    DisarmReady = reader.ReadBool();
                    StunReady = reader.ReadBool();

                    goto case 4;
                }
            case 4:
                {
                    if (version <= 25)
                    {
                        reader.ReadPoison();
                    }

                    goto case 3;
                }
            case 3:
                {
                    m_StatCap = reader.ReadInt();

                    goto case 2;
                }
            case 2:
                {
                    NameHue = reader.ReadInt();

                    goto case 1;
                }
            case 1:
                {
                    m_Hunger = reader.ReadInt();

                    goto case 0;
                }
            case 0:
                {
                    if (version < 11)
                    {
                        m_DisplayGuildTitle = true;
                    }

                    if (version < 3)
                    {
                        m_StatCap = 225;
                    }

                    if (version < 15)
                    {
                        m_Followers = 0;
                        m_FollowersMax = 5;
                    }

                    m_Location = reader.ReadPoint3D();
                    m_Body = new Body(reader.ReadInt());
                    m_Name = reader.ReadString();
                    m_GuildTitle = reader.ReadString();
                    m_Criminal = reader.ReadBool();
                    m_Kills = reader.ReadInt();
                    SpeechHue = reader.ReadInt();
                    EmoteHue = reader.ReadInt();
                    WhisperHue = reader.ReadInt();
                    YellHue = reader.ReadInt();
                    m_Language = reader.ReadString();
                    m_Female = reader.ReadBool();
                    m_Warmode = reader.ReadBool();
                    m_Hidden = reader.ReadBool();
                    m_Direction = (Direction)reader.ReadByte();
                    m_Hue = reader.ReadInt();
                    m_Str = reader.ReadInt();
                    m_Dex = reader.ReadInt();
                    m_Int = reader.ReadInt();
                    m_Hits = reader.ReadInt();
                    m_Stam = reader.ReadInt();
                    m_Mana = reader.ReadInt();
                    m_Map = reader.ReadMap();
                    m_Blessed = reader.ReadBool();
                    m_Fame = reader.ReadInt();
                    m_Karma = reader.ReadInt();
                    m_AccessLevel = (AccessLevel)reader.ReadByte();

                    Skills = new Skills(this, reader);

                    Items = reader.ReadEntityList<Item>();

                    m_Player = reader.ReadBool();
                    m_Title = reader.ReadString();
                    Profile = reader.ReadString();
                    ProfileLocked = reader.ReadBool();

                    if (version <= 18)
                    {
                        reader.ReadInt();
                        reader.ReadInt();
                        reader.ReadInt();
                    }

                    AutoPageNotify = reader.ReadBool();

                    LogoutLocation = reader.ReadPoint3D();
                    LogoutMap = reader.ReadMap();

                    m_StrLock = (StatLockType)reader.ReadByte();
                    m_DexLock = (StatLockType)reader.ReadByte();
                    m_IntLock = (StatLockType)reader.ReadByte();

                    _statMods = new List<StatMod>();
                    _skillMods = new List<SkillMod>();

                    if (version < 32)
                    {
                        if (reader.ReadBool())
                        {
                            var count = reader.ReadInt();
                            for (var i = 0; i < count; ++i)
                            {
                                reader.ReadDateTime();
                            }
                        }
                    }

                    if (m_Player && m_Map != Map.Internal)
                    {
                        LogoutLocation = m_Location;
                        LogoutMap = m_Map;

                        m_Map = Map.Internal;
                    }

                    m_Map?.OnEnter(this);

                    if (m_Criminal)
                    {
                        Timer.StartTimer(ExpireCriminalDelay, ExpireCriminal, out _expireCriminalTimerToken);
                    }

                    if (ShouldCheckStatTimers)
                    {
                        CheckStatTimers();
                    }

                    UpdateRegion();

                    UpdateResistances();

                    break;
                }
        }

        if (!m_Player)
        {
            Utility.Intern(ref m_Name);
        }

        Utility.Intern(ref m_Title);
        Utility.Intern(ref m_Language);
    }

    public void ConvertHair()
    {
        Item hair;

        if ((hair = FindItemOnLayer(Layer.Hair)) != null)
        {
            HairItemID = hair.ItemID;
            HairHue = hair.Hue;
            hair.Delete();
        }

        if ((hair = FindItemOnLayer(Layer.FacialHair)) != null)
        {
            FacialHairItemID = hair.ItemID;
            FacialHairHue = hair.Hue;
            hair.Delete();
        }
    }

    public virtual void CheckStatTimers()
    {
        if (Deleted)
        {
            return;
        }

        if (Hits < HitsMax)
        {
            if (CanRegenHits)
            {
                m_HitsTimer ??= new HitsTimer(this);
                m_HitsTimer.Start();
            }
            else
            {
                m_HitsTimer?.Stop();
            }
        }
        else
        {
            Hits = HitsMax;
        }

        if (Stam < StamMax)
        {
            if (CanRegenStam)
            {
                m_StamTimer ??= new StamTimer(this);
                m_StamTimer.Start();
            }
            else
            {
                m_StamTimer?.Stop();
            }
        }
        else
        {
            Stam = StamMax;
        }

        if (Mana < ManaMax)
        {
            if (CanRegenMana)
            {
                m_ManaTimer ??= new ManaTimer(this);
                m_ManaTimer.Start();
            }
            else
            {
                m_ManaTimer?.Stop();
            }
        }
        else
        {
            Mana = ManaMax;
        }
    }

    public static string GetAccessLevelName(AccessLevel level) => m_AccessLevelNames[(int)level];

    public virtual bool CanPaperdollBeOpenedBy(Mobile from) => Body.IsHuman || Body.IsGhost || IsBodyMod;

    public virtual void GetChildContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list, Item item)
    {
    }

    public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
    {
    }

    public void Internalize()
    {
        Map = Map.Internal;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="AddItem">added</see> from the Mobile,
    ///     such
    ///     as when it is equipped.
    ///     <seealso cref="Items" />
    ///     <seealso cref="OnItemRemoved" />
    /// </summary>
    public virtual void OnItemAdded(Item item)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="RemoveItem">removed</see> from the
    ///     Mobile.
    ///     <seealso cref="Items" />
    ///     <seealso cref="OnItemAdded" />
    /// </summary>
    public virtual void OnItemRemoved(Item item)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="item" /> is becomes a child of the Mobile; it's worn or
    ///     contained
    ///     at some level of the Mobile's <see cref="Mobile.Backpack">backpack</see> or <see cref="Mobile.BankBox">bank box</see>
    ///     <seealso cref="OnSubItemRemoved" />
    ///     <seealso cref="OnItemAdded" />
    /// </summary>
    public virtual void OnSubItemAdded(Item item)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="item" /> is removed from the Mobile, its
    ///     <see cref="Mobile.Backpack">backpack</see>, or its <see cref="Mobile.BankBox">bank box</see>.
    ///     <seealso cref="OnSubItemAdded" />
    ///     <seealso cref="OnItemRemoved" />
    /// </summary>
    public virtual void OnSubItemRemoved(Item item)
    {
    }

    public virtual void OnItemBounceCleared(Item item)
    {
    }

    public virtual void OnSubItemBounceCleared(Item item)
    {
    }

    public void AddItem(Item item)
    {
        if (item?.Deleted != false)
        {
            return;
        }

        if (item.Parent == this)
        {
            return;
        }

        if (item.Parent is Mobile parentMobile)
        {
            parentMobile.RemoveItem(item);
        }
        else if (item.Parent is Item parentItem)
        {
            parentItem.RemoveItem(item);
        }
        else
        {
            item.SendRemovePacket();
        }

        item.Parent = this;
        item.Map = m_Map;

        Items.Add(item);

        if (!item.IsVirtualItem)
        {
            UpdateTotal(item, TotalType.Gold, item.TotalGold);
            UpdateTotal(item, TotalType.Items, item.TotalItems + 1);
            UpdateTotal(item, TotalType.Weight, item.TotalWeight + item.PileWeight);
        }

        item.Delta(ItemDelta.Update);

        item.OnAdded(this);
        OnItemAdded(item);

        if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
            item.PoisonResistance != 0 || item.EnergyResistance != 0)
        {
            UpdateResistances();
        }
    }

    public void RemoveItem(Item item)
    {
        if (item == null || Items == null)
        {
            return;
        }

        if (Items.Remove(item))
        {
            item.SendRemovePacket();

            if (!item.IsVirtualItem)
            {
                UpdateTotal(item, TotalType.Gold, -item.TotalGold);
                UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1));
                UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight));
            }

            item.Parent = null;

            item.OnRemoved(this);
            OnItemRemoved(item);

            if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
                item.PoisonResistance != 0 || item.EnergyResistance != 0)
            {
                UpdateResistances();
            }
        }
    }

    public virtual void Animate(int action, int frameCount, int repeatCount, bool forward, bool repeat, int delay)
    {
        var map = m_Map;

        if (map == null)
        {
            return;
        }

        ProcessDelta();

        Span<byte> buffer = stackalloc byte[OutgoingMobilePackets.MobileAnimationPacketLength].InitializePacket();

        foreach (var state in map.GetClientsInRange(m_Location))
        {
            if (!state.Mobile.CanSee(this))
            {
                continue;
            }

            state.Mobile.ProcessDelta();

            if (Body.IsGargoyle)
            {
                frameCount = 10;

                if (Flying)
                {
                    action = action switch
                    {
                        >= 9 and <= 11    => 71,
                        >= 12 and <= 14   => 72,
                        20                => 77,
                        31                => 71,
                        34                => 78,
                        >= 200 and <= 259 => 75,
                        >= 260 and <= 270 => 75,
                        _                 => action
                    };
                }
                else
                {
                    action = action switch
                    {
                        >= 200 and <= 259 => 17,
                        >= 260 and <= 270 => 16,
                        _                 => action
                    };
                }
            }

            OutgoingMobilePackets.CreateMobileAnimation(
                buffer,
                Serial,
                action,
                frameCount,
                repeatCount,
                forward,
                repeat,
                delay
            );

            state.Send(buffer);
        }
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void SendSound(int soundID)
    {
        m_NetState.SendSoundEffect(soundID, this);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public void SendSound(int soundID, IPoint3D p)
    {
        m_NetState.SendSoundEffect(soundID, p);
    }

    /**
         * Plays a sound to netstates that can see this mobile
         */
    public void PlaySound(int soundID)
    {
        if (soundID == -1 || m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingEffectPackets.SoundPacketLength].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (state.Mobile.CanSee(this))
            {
                OutgoingEffectPackets.CreateSoundEffect(buffer, soundID, this);
                state.Send(buffer);
            }
        }
    }

    public virtual void SendOPLPacketTo(NetState ns)
    {
        if (ObjectPropertyList.Enabled)
        {
            ns.SendOPLInfo(this);
        }
    }

    public virtual void OnAccessLevelChanged(AccessLevel oldLevel)
    {
    }

    public virtual void OnFameChange(int oldValue)
    {
    }

    public virtual void OnKarmaChange(int oldValue)
    {
    }

    // Mobile did something which should unhide him
    public virtual void RevealingAction()
    {
        if (m_Hidden && m_AccessLevel == AccessLevel.Player)
        {
            Hidden = false;
        }

        DisruptiveAction(); // Anything that unhides you will also distrupt meditation
    }

    public void SendRemovePacket(bool everyone = true)
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (state != m_NetState && (everyone || !state.Mobile.CanSee(this)))
            {
                OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
                state.Send(removeEntity);
            }
        }
    }

    public void ClearScreen()
    {
        if (m_Map == null || m_NetState == null)
        {
            return;
        }

        foreach (var m in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
        {
            if (m != this && Utility.InUpdateRange(m_Location, m.m_Location))
            {
                m_NetState.SendRemoveEntity(m.Serial);
            }
        }

        foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
        {
            if (InRange(item.Location, item.GetUpdateRange(this)))
            {
                m_NetState.SendRemoveEntity(item.Serial);
            }
        }
    }

    /// <summary>
    ///     Overridable. Event invoked before the Mobile says something.
    ///     <seealso cref="DoSpeech" />
    /// </summary>
    public virtual void OnSaid(SpeechEventArgs e)
    {
        if (Squelched)
        {
            if (Core.ML)
            {
                SendLocalizedMessage(500168); // You can not say anything, you have been muted.
            }
            else
            {
                SendMessage("You can not say anything, you have been squelched."); // Cliloc ITSELF changed during ML.
            }

            e.Blocked = true;
        }

        if (!e.Blocked)
        {
            RevealingAction();
        }
    }

    public virtual bool HandlesOnSpeech(Mobile from) => false;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile hears speech. This event will only be invoked if
    ///     <see cref="HandlesOnSpeech" /> returns true.
    ///     <seealso cref="DoSpeech" />
    /// </summary>
    public virtual void OnSpeech(SpeechEventArgs e)
    {
    }

    public void SendEverything()
    {
        var ns = m_NetState;

        if (m_Map == null || ns == null)
        {
            return;
        }

        foreach (var m in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
        {
            if (CanSee(m) && Utility.InUpdateRange(m_Location, m.m_Location))
            {
                ns.SendMobileIncoming(this, m);

                if (ns.StygianAbyss)
                {
                    ns.SendMobileHealthbar(m, Healthbar.Yellow);
                    ns.SendMobileHealthbar(m, Healthbar.Poison);
                }

                if (m.IsDeadBondedPet)
                {
                    ns.SendBondedStatus(m.Serial, true);
                }

                m.SendOPLPacketTo(ns);
            }
        }

        foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
        {
            if (CanSee(item) && InRange(item.Location, item.GetUpdateRange(this)))
            {
                item.SendInfoTo(ns);
            }
        }

        var range = new Rectangle2D(m_Location.X - Core.GlobalMaxUpdateRange,
            m_Location.Y - Core.GlobalMaxUpdateRange,
            Core.GlobalMaxUpdateRange * 2 + 1,
            Core.GlobalMaxUpdateRange * 2 + 1);

        foreach (var multi in m_Map.GetMultisInBounds(range))
        {
            multi.SendInfoTo(ns);
        }
    }

    public void UpdateRegion()
    {
        if (Deleted)
        {
            return;
        }

        var newRegion = Region.Find(m_Location, m_Map);

        if (newRegion != m_Region)
        {
            Region.OnRegionChange(this, m_Region, newRegion);

            var oldRegion = m_Region;
            m_Region = newRegion;
            OnRegionChange(oldRegion, newRegion);
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <see cref="Map" /> changes.
    /// </summary>
    protected virtual void OnMapChange(Map oldMap)
    {
    }

    public void SetDirection(Direction dir)
    {
        m_Direction = dir;
    }

    public virtual int GetSeason() => m_Map?.Season ?? 1;

    public virtual int GetPacketFlags(bool stygianAbyss)
    {
        var flags = 0x0;

        if (m_Paralyzed || m_Frozen || _disableCastParalyze && m_Spell?.BlocksMovement == true)
        {
            flags |= 0x01;
        }

        if (m_Female)
        {
            flags |= 0x02;
        }

        if (stygianAbyss)
        {
            if (m_Flying)
            {
                flags |= 0x04;
            }
        }
        else
        {
            if (m_Poison != null)
            {
                flags |= 0x04;
            }
        }

        if (m_Blessed || m_YellowHealthbar)
        {
            flags |= 0x08;
        }

        if (IgnoreMobiles)
        {
            flags |= 0x10;
        }

        if (m_Warmode)
        {
            flags |= 0x40;
        }

        if (m_Hidden)
        {
            flags |= 0x80;
        }

        return flags;
    }

    public virtual void OnGenderChanged(bool oldFemale)
    {
    }

    public virtual void ToggleFlying()
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked after the Warmode property has changed.
    /// </summary>
    public virtual void OnWarmodeChanged()
    {
    }

    public virtual void OnHiddenChanged()
    {
        AllowedStealthSteps = 0;

        if (m_Map == null)
        {
            return;
        }

        Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            var m = state.Mobile;
            if (!m.CanSee(this))
            {
                OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
                state.Send(removeEntity);
            }
            else
            {
                state.SendMobileIncoming(m, this);

                if (IsDeadBondedPet)
                {
                    state.SendBondedStatus(Serial, true);
                }

                SendOPLPacketTo(state);
            }
        }
    }

    public virtual void OnConnected()
    {
    }

    public virtual void OnBeforeDisconnected()
    {
    }

    public virtual void OnDisconnected()
    {
    }

    public virtual void OnNetStateChanged()
    {
    }

    public virtual bool CanSee(object o)
    {
        return o switch
        {
            Item item     => CanSee(item),
            Mobile mobile => CanSee(mobile),
            _             => true
        };
    }

    public virtual bool CanSee(Item item)
    {
        if (m_Map == Map.Internal)
        {
            return false;
        }

        if (item.Map == Map.Internal)
        {
            return false;
        }

        if (item.Parent != null)
        {
            if (item.Parent is Item parent)
            {
                if (!(CanSee(parent) && parent.IsChildVisibleTo(this, item)))
                {
                    return false;
                }
            }
            else if (item.Parent is Mobile mobile)
            {
                if (!CanSee(mobile))
                {
                    return false;
                }
            }
        }

        if (item is BankBox box && m_AccessLevel <= AccessLevel.Counselor && (box.Owner != this || !box.Opened))
        {
            return false;
        }

        if (item is SecureTradeContainer container)
        {
            var trade = container.Trade;

            if (trade != null && trade.From.Mobile != this && trade.To.Mobile != this)
            {
                return false;
            }
        }

        return !item.Deleted && item.Map == m_Map && (item.Visible || item.CanSeeStaffOnly(this));
    }

    public virtual bool CanSee(Mobile m)
    {
        if (Deleted || m.Deleted || m_Map == Map.Internal || m.m_Map == Map.Internal)
        {
            return false;
        }

        return this == m || m.m_Map == m_Map &&
            (!m.Hidden || m_AccessLevel != AccessLevel.Player &&
                (m_AccessLevel >= m.AccessLevel || m_AccessLevel >= AccessLevel.Administrator)) &&
            (m.Alive || Core.SE && Skills.SpiritSpeak.Value >= 100.0 || !Alive ||
             m_AccessLevel > AccessLevel.Player || m.Warmode);
    }

    public virtual bool CanBeRenamedBy(Mobile from) =>
        from.AccessLevel >= AccessLevel.GameMaster && from.m_AccessLevel > m_AccessLevel;

    public virtual void OnGuildTitleChange(string oldTitle)
    {
    }

    public virtual void OnAfterNameChange(string oldName, string newName)
    {
    }

    public virtual void OnGuildChange(BaseGuild oldGuild)
    {
    }

    public virtual int SafeBody(int body)
    {
        var delta = -1;

        for (var i = 0; delta < 0 && i < m_InvalidBodies.Length; ++i)
        {
            delta = m_InvalidBodies[i] - body;
        }

        return delta != 0 ? body : 0;
    }

    private ObjectPropertyList InitializePropertyList(ObjectPropertyList list)
    {
        GetProperties(list);
        list.Terminate();
        return list;
    }

    public void ClearProperties()
    {
        m_PropertyList = null;
    }

#nullable enable
    public void InvalidateProperties()
    {
        if (!ObjectPropertyList.Enabled)
        {
            return;
        }

        if (m_Map != null && m_Map != Map.Internal && !World.Loading)
        {
            int? oldHash;
            int newHash;
            if (m_PropertyList != null)
            {
                oldHash = m_PropertyList.Hash;
                m_PropertyList.Reset();
                InitializePropertyList(m_PropertyList);
                newHash = m_PropertyList.Hash;
            }
            else
            {
                oldHash = null;
                newHash = PropertyList.Hash;
            }

            if (oldHash != newHash)
            {
                Delta(MobileDelta.Properties);
            }
        }
        else
        {
            ClearProperties();
        }
    }
#nullable restore

    public virtual void SetLocation(Point3D newLocation, bool isTeleport)
    {
        if (Deleted)
        {
            return;
        }

        var oldLocation = m_Location;

        if (oldLocation == newLocation)
        {
            return;
        }

        m_Location = newLocation;
        m_Map?.OnMove(oldLocation, this);
        UpdateRegion();

        var box = FindBankNoCreate();

        if (box?.Opened == true)
        {
            box.Close();
        }

        if (m_NetState != null)
        {
            m_NetState.ValidateAllTrades();

            if (isTeleport && (!m_NetState.HighSeas || !NoMoveHS))
            {
                m_NetState.Sequence = 0;
                m_NetState.SendMobileUpdate(this);
            }
        }

        var map = m_Map;

        if (map != null)
        {
            // First, send a remove message to everyone who can no longer see us. (inOldRange && !inNewRange)
            Span<byte> removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket();

            foreach (var ns in map.GetClientsInRange(oldLocation))
            {
                if (ns != m_NetState && !Utility.InUpdateRange(newLocation, ns.Mobile.Location))
                {
                    OutgoingEntityPackets.CreateRemoveEntity(removeEntity, Serial);
                    ns.Send(removeEntity);
                }
            }

            var ourState = m_NetState;

            // Check to see if we are attached to a client
            if (ourState != null)
            {
                // We are attached to a client, so it's a bit more complex. We need to send new items and people to ourself, and ourself to other clients

                foreach (var m in map.GetMobilesInRange(newLocation, Core.GlobalMaxUpdateRange))
                {
                    if (m == this)
                    {
                        continue;
                    }

                    if (!Utility.InUpdateRange(newLocation, m.m_Location))
                    {
                        continue;
                    }

                    var inOldRange = Utility.InUpdateRange(oldLocation, m.m_Location);
                    var ns = m.m_NetState;

                    if (ns != null &&
                        (isTeleport && (!ns.HighSeas || !NoMoveHS) || !inOldRange) && m.CanSee(this))
                    {
                        ns.SendMobileIncoming(m, this);

                        if (ns.StygianAbyss)
                        {
                            ns.SendMobileHealthbar(this, Healthbar.Poison);
                            ns.SendMobileHealthbar(this, Healthbar.Yellow);
                        }

                        if (IsDeadBondedPet)
                        {
                            ns.SendBondedStatus(Serial, true);
                        }

                        SendOPLPacketTo(ns);
                    }

                    if (inOldRange || !CanSee(m))
                    {
                        continue;
                    }

                    ourState.SendMobileIncoming(this, m);

                    if (ourState.StygianAbyss)
                    {
                        ourState.SendMobileHealthbar(m, Healthbar.Poison);
                        ourState.SendMobileHealthbar(m, Healthbar.Yellow);
                    }

                    if (m.IsDeadBondedPet)
                    {
                        ourState.SendBondedStatus(m.Serial, true);
                    }

                    m.SendOPLPacketTo(ourState);
                }

                foreach (var item in map.GetItemsInRange(newLocation, Core.GlobalMaxUpdateRange))
                {
                    var range = item.GetUpdateRange(this);
                    var loc = item.Location;

                    if (!Utility.InRange(oldLocation, loc, range) && Utility.InRange(newLocation, loc, range) &&
                        CanSee(item))
                    {
                        item.SendInfoTo(ourState);
                    }
                }
            }
            else
            {
                // We're not attached to a client, so simply send an Incoming
                foreach (var ns in map.GetClientsInRange(newLocation))
                {
                    var m = ns.Mobile;
                    if ((isTeleport && (!ns.HighSeas || !NoMoveHS) || !Utility.InUpdateRange(oldLocation, m.Location)) &&
                        m.CanSee(this))
                    {
                        ns.SendMobileIncoming(m, this);

                        if (ns.StygianAbyss)
                        {
                            ns.SendMobileHealthbar(this, Healthbar.Poison);
                            ns.SendMobileHealthbar(this, Healthbar.Yellow);
                        }

                        if (IsDeadBondedPet)
                        {
                            ns.SendBondedStatus(Serial, true);
                        }

                        SendOPLPacketTo(ns);
                    }
                }
            }
        }

        OnLocationChange(oldLocation);

        Region.OnLocationChanged(this, oldLocation);
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <see cref="Location" /> changes.
    /// </summary>
    protected virtual void OnLocationChange(Point3D oldLocation)
    {
    }

    public bool HasFreeHand() => FindItemOnLayer(Layer.TwoHanded) == null;

    public virtual IWeapon GetDefaultWeapon() => DefaultWeapon;

    public BankBox FindBankNoCreate()
    {
        if (m_BankBox?.Deleted != false || m_BankBox.Parent != this)
        {
            m_BankBox = FindItemOnLayer<BankBox>(Layer.Bank);
        }

        return m_BankBox;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public T FindItemOnLayer<T>(Layer layer) where T : Item => FindItemOnLayer(layer) as T;

    public Item FindItemOnLayer(Layer layer)
    {
        var eq = Items;
        var count = eq.Count;

        for (var i = 0; i < count; ++i)
        {
            var item = eq[i];

            // TODO: We only allow 1 item per layer. It's an implicit contract.
            if (!item.Deleted && item.Layer == layer)
            {
                return item;
            }
        }

        return null;
    }

    public void SendIncomingPacket()
    {
        if (m_Map == null)
        {
            return;
        }

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            var m = state.Mobile;
            if (m.CanSee(this))
            {
                state.SendMobileIncoming(m, this);

                if (state.StygianAbyss)
                {
                    state.SendMobileHealthbar(this, Healthbar.Poison);
                    state.SendMobileHealthbar(this, Healthbar.Yellow);
                }

                if (IsDeadBondedPet)
                {
                    state.SendBondedStatus(Serial, true);
                }

                SendOPLPacketTo(state);
            }
        }
    }

    public bool PlaceInBackpack(Item item) =>
        !item.Deleted && Backpack?.TryDropItem(this, item, false) == true;

    public bool AddToBackpack(Item item)
    {
        if (item.Deleted)
        {
            return false;
        }

        if (!PlaceInBackpack(item))
        {
            var loc = m_Location;
            var map = m_Map;

            if ((map == null || map == Map.Internal) && LogoutMap != null)
            {
                loc = LogoutLocation;
                map = LogoutMap;
            }

            item.MoveToWorld(loc, map);
            return false;
        }

        return true;
    }

    public virtual bool CheckLift(Mobile from, Item item, ref LRReason reject) => true;

    public virtual bool CheckNonlocalLift(Mobile from, Item item) =>
        from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;

    public virtual bool CheckTrade(
        Mobile to, Item item, SecureTradeContainer cont, bool message, bool checkItems, int plusItems, int plusWeight
    ) => true;

    public virtual bool OpenTrade(Mobile from, Item offer = null)
    {
        if (!from.Player || !Player || !from.Alive || !Alive)
        {
            return false;
        }

        var ourState = m_NetState;
        var theirState = from.m_NetState;

        if (ourState == null || theirState == null)
        {
            return false;
        }

        var cont = theirState.FindTradeContainer(this);

        if (!from.CheckTrade(this, offer, cont, true, true, 0, 0))
        {
            return false;
        }

        cont ??= theirState.AddTrade(ourState);

        if (offer != null)
        {
            cont.DropItem(offer);
        }

        return true;
    }

    /// <summary>
    ///     Overridable. Event invoked when a Mobile (<paramref name="from" />) drops an
    ///     <see cref="Item">
    ///         <paramref name="dropped" />
    ///     </see>
    ///     onto the Mobile.
    /// </summary>
    public virtual bool OnDragDrop(Mobile from, Item dropped)
    {
        if (from == this)
        {
            var pack = Backpack;
            return pack != null && dropped.DropToItem(from, pack, new Point3D(-1, -1, 0));
        }

        return from.InRange(Location, 2) && OpenTrade(from, dropped);
    }

    public virtual bool CheckEquip(Item item)
    {
        for (var i = 0; i < Items.Count; ++i)
        {
            if (Items[i].CheckConflictingLayer(this, item, item.Layer) ||
                item.CheckConflictingLayer(this, Items[i], Items[i].Layer))
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to wear <paramref name="item" />.
    /// </summary>
    /// <returns>True if the request is accepted, false if otherwise.</returns>
    public virtual bool OnEquip(Item item)
    {
        // For some reason OSI allows equipping quest items, but they are unmarked in the process
        if (item.QuestItem)
        {
            item.QuestItem = false;

            // An item must be in your backpack (and not in a container within) to be toggled as a quest item.
            SendLocalizedMessage(1074769);
        }

        return true;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to lift <paramref name="item" />.
    /// </summary>
    /// <returns>True if the lift is allowed, false if otherwise.</returns>
    /// <example>
    ///     The following example demonstrates usage. It will disallow any attempts to pick up a pick axe if the Mobile does not
    ///     have
    ///     enough strength.
    ///     <code>
    ///   public override bool OnDragLift( Item item )
    ///   {
    ///     if (item is Pickaxe &amp;&amp; this.Str &lt; 60)
    ///     {
    ///       SendMessage( "That is too heavy for you to lift." );
    ///       return false;
    ///     }
    ///
    ///     return base.OnDragLift( item );
    ///   }</code>
    /// </example>
    public virtual bool OnDragLift(Item item) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into a
    ///     <see cref="Container">
    ///         <paramref name="container" />
    ///     </see>
    ///     .
    /// </summary>
    /// <returns>True if the drop is allowed, false if otherwise.</returns>
    public virtual bool OnDroppedItemInto(Item item, Container container, Point3D loc) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> directly onto another
    ///     <see cref="Item" />, <paramref name="target" />. This is the case of stacking items.
    /// </summary>
    /// <returns>True if the drop is allowed, false if otherwise.</returns>
    public virtual bool OnDroppedItemOnto(Item item, Item target) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into another
    ///     <see cref="Item" />, <paramref name="target" />. The target item is most likely a <see cref="Container" />.
    /// </summary>
    /// <returns>True if the drop is allowed, false if otherwise.</returns>
    public virtual bool OnDroppedItemToItem(Item item, Item target, Point3D loc) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to give <paramref name="item" /> to a Mobile (
    ///     <paramref name="target" />).
    /// </summary>
    /// <returns>True if the drop is allowed, false if otherwise.</returns>
    public virtual bool OnDroppedItemToMobile(Item item, Mobile target) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> to the world at a
    ///     <see cref="Point3D">
    ///         <paramref name="location" />
    ///     </see>
    ///     .
    /// </summary>
    /// <returns>True if the drop is allowed, false if otherwise.</returns>
    public virtual bool OnDroppedItemToWorld(Item item, Point3D location) => true;

    /// <summary>
    ///     Overridable. Virtual event when <paramref name="from" /> successfully uses <paramref name="item" /> while it's on this
    ///     Mobile.
    ///     <seealso cref="Item.OnItemUsed" />
    /// </summary>
    public virtual void OnItemUsed(Mobile from, Item item)
    {
    }

    public virtual bool CheckNonlocalDrop(Mobile from, Item item, Item target) =>
        from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;

    public virtual bool CheckItemUse(Mobile from, Item item) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when <paramref name="from" /> successfully lifts <paramref name="item" /> from this
    ///     Mobile.
    ///     <seealso cref="Item.OnItemLifted" />
    /// </summary>
    public virtual void OnItemLifted(Mobile from, Item item)
    {
    }

    public virtual bool AllowItemUse(Item item) => true;

    public virtual bool AllowEquipFrom(Mobile mob) =>
        mob == this || mob.AccessLevel >= AccessLevel.GameMaster && mob.AccessLevel > AccessLevel;

    public virtual bool EquipItem(Item item)
    {
        if (item?.Deleted != false || !item.CanEquip(this))
        {
            return false;
        }

        if (CheckEquip(item) && OnEquip(item) && item.OnEquip(this))
        {
            if (m_Spell?.OnCasterEquipping(item) == false)
            {
                return false;
            }

            // if (m_Spell != null && m_Spell.State == SpellState.Casting)
            // m_Spell.Disturb( DisturbType.EquipRequest );

            AddItem(item);
            return true;
        }

        return false;
    }

    public void DefaultMobileInit()
    {
        m_StatCap = 225;
        m_FollowersMax = 5;
        Skills = new Skills(this);
        Items = new List<Item>();
        _statMods = new List<StatMod>();
        _skillMods = new List<SkillMod>();
        Map = Map.Internal;
        AutoPageNotify = true;
        Aggressors = new List<AggressorInfo>();
        Aggressed = new List<AggressorInfo>();
        DamageEntries = new List<DamageEntry>();

        NextSkillTime = Core.TickCount;
    }

    public virtual void Delta(MobileDelta flag)
    {
#if THREADGUARD
            if (Thread.CurrentThread != Core.Thread)
            {
                Utility.PushColor(ConsoleColor.Red);
                Console.WriteLine("Attempting to queue a delta change from an invalid thread!");
                Console.WriteLine(new StackTrace());
                Utility.PopColor();
                return;
            }
#endif

        if (m_Map == null || m_Map == Map.Internal || Deleted)
        {
            return;
        }

        m_DeltaFlags |= flag;

        if (!m_InDeltaQueue)
        {
            m_InDeltaQueue = true;
            m_DeltaQueue.Enqueue(this);
        }
    }

    public static void ProcessDeltaQueue()
    {
        var limit = m_DeltaQueue.Count;

        while (m_DeltaQueue.Count > 0 && --limit >= 0)
        {
            var mob = m_DeltaQueue.Dequeue();

            if (mob == null)
            {
                continue;
            }

            mob.m_InDeltaQueue = false;

            try
            {
                mob.ProcessDelta();
            }
            catch (Exception ex)
            {
                logger.Debug(ex, "Process Delta Queue for {Mobile} failed", mob);
            }
        }

        if (m_DeltaQueue.Count > 0)
        {
            logger.Warning("{Count} mobiles left in delta queue after processing.", m_DeltaQueue.Count);
        }
    }

    public virtual void OnKillsChange(int oldValue)
    {
    }

    public bool CheckAlive(bool message = true)
    {
        if (Alive)
        {
            return true;
        }

        if (message)
        {
            LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019048); // I am dead and cannot do that.
        }

        return false;
    }

    public void LaunchBrowser(string url)
    {
        m_NetState?.LaunchBrowser(url);
    }

    public void InitStats(int str, int dex, int intel)
    {
        m_Str = str;
        m_Dex = dex;
        m_Int = intel;

        Hits = HitsMax;
        Stam = StamMax;
        Mana = ManaMax;

        Delta(MobileDelta.Stat | MobileDelta.Hits | MobileDelta.Stam | MobileDelta.Mana);
    }

    public virtual void DisplayPaperdollTo(Mobile to)
    {
        EventSink.InvokePaperdollRequest(to, this);
    }

    /// <summary>
    ///     Overridable. Event invoked when the Mobile requests to open his own paperdoll via the 'Open Paperdoll' macro.
    /// </summary>
    public virtual void OnPaperdollRequest()
    {
        if (CanPaperdollBeOpenedBy(this))
        {
            DisplayPaperdollTo(this);
        }
    }

    /// <summary>
    ///     Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's stats.
    /// </summary>
    /// <param name="from"></param>
    public virtual void OnStatsQuery(Mobile from)
    {
        if (from.Map == Map && Utility.InUpdateRange(Location, from.Location) && from.CanSee(this))
        {
            from.m_NetState.SendMobileStatus(from, this);
        }

        if (from == this)
        {
            m_NetState.SendStatLockInfo(this);
        }

        if (Party is IParty ip)
        {
            ip.OnStatsQuery(from, this);
        }
    }

    /// <summary>
    ///     Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's skills.
    /// </summary>
    public virtual void OnSkillsQuery(Mobile from)
    {
        if (from == this)
        {
            m_NetState.SendSkillsUpdate(Skills);
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <see cref="Region" /> changes.
    /// </summary>
    public virtual void OnRegionChange(Region old, Region @new)
    {
    }

    /// <summary>
    ///     Overridable. Event invoked when the Mobile is single clicked.
    /// </summary>
    public virtual void OnSingleClick(Mobile from)
    {
        if (Deleted || AccessLevel == AccessLevel.Player && DisableHiddenSelfClick && Hidden && from == this)
        {
            return;
        }

        if (GuildClickMessage)
        {
            var guild = m_Guild;

            if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular))
            {
                var title = GuildTitle?.Trim() ?? "";
                string type;

                if (guild.Type >= 0 && (int)guild.Type < m_GuildTypes.Length)
                {
                    type = m_GuildTypes[(int)guild.Type];
                }
                else
                {
                    type = "";
                }

                var text = string.Format(
                    title.Length <= 0 ? "[{1}]{2}" : "[{0}, {1}]{2}",
                    title,
                    guild.Abbreviation,
                    type
                );

                PrivateOverheadMessage(MessageType.Regular, SpeechHue, true, text, from.NetState);
            }
        }

        int hue;

        if (NameHue != -1)
        {
            hue = NameHue;
        }
        else if (AccessLevel > AccessLevel.Player)
        {
            hue = 11;
        }
        else
        {
            hue = Notoriety.GetHue(Notoriety.Compute(from, this));
        }

        var name = Name ?? "";

        var prefix = "";

        if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000)
        {
            prefix = m_Female ? "Lady" : "Lord";
        }

        var suffix = ApplyNameSuffix(ClickTitle && !string.IsNullOrEmpty(Title) ? Title : "");

        string val;

        if (prefix.Length > 0 && suffix.Length > 0)
        {
            val = $"{prefix} {name} {suffix}";
        }
        else if (prefix.Length > 0)
        {
            val = $"{prefix} {name}";
        }
        else if (suffix.Length > 0)
        {
            val = $"{name} {suffix}";
        }
        else
        {
            val = name;
        }

        PrivateOverheadMessage(MessageType.Label, hue, AsciiClickMessage, val, from.NetState);
    }

    public bool CheckSkill(SkillName skill, double minSkill, double maxSkill) =>
        SkillCheckLocationHandler?.Invoke(this, skill, minSkill, maxSkill) == true;

    public bool CheckSkill(SkillName skill, double chance) =>
        SkillCheckDirectLocationHandler?.Invoke(this, skill, chance) == true;

    public bool CheckTargetSkill(SkillName skill, object target, double minSkill, double maxSkill) =>
        SkillCheckTargetHandler?.Invoke(this, skill, target, minSkill, maxSkill) == true;

    public bool CheckTargetSkill(SkillName skill, object target, double chance) =>
        SkillCheckDirectTargetHandler?.Invoke(this, skill, target, chance) == true;

    public virtual void DisruptiveAction()
    {
        if (Meditating)
        {
            Meditating = false;
            SendLocalizedMessage(500134); // You stop meditating.
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the sector this Mobile is in gets <see cref="Map.Sector.Activate">activated</see>.
    /// </summary>
    public virtual void OnSectorActivate()
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the sector this Mobile is in gets
    ///     <see cref="Map.Sector.Deactivate">deactivated</see>.
    /// </summary>
    public virtual void OnSectorDeactivate()
    {
    }

    public static TimeSpan GetHitsRegenRate(Mobile m) => HitsRegenRateHandler?.Invoke(m) ?? DefaultHitsRate;

    public static TimeSpan GetStamRegenRate(Mobile m) => StamRegenRateHandler?.Invoke(m) ?? DefaultStamRate;

    public static TimeSpan GetManaRegenRate(Mobile m) => ManaRegenRateHandler?.Invoke(m) ?? DefaultManaRate;

    public static char[] DefaultGhostChars = { 'o', 'O' };

    public Prompt BeginPrompt(PromptCallback callback, PromptCallback cancelCallback) =>
        Prompt = new SimplePrompt(callback, cancelCallback);

    public Prompt BeginPrompt(PromptCallback callback, bool callbackHandlesCancel = false) =>
        Prompt = new SimplePrompt(callback, callbackHandlesCancel);

    public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, PromptStateCallback<T> cancelCallback, T state) =>
        Prompt = new SimpleStatePrompt<T>(callback, cancelCallback, state);

    public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, bool callbackHandlesCancel, T state) =>
        Prompt = new SimpleStatePrompt<T>(callback, callbackHandlesCancel, state);

    public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, T state) =>
        BeginPrompt(callback, false, state);

    public virtual int GetAngerSound()
    {
        if (BaseSoundID != 0)
        {
            return BaseSoundID;
        }

        return -1;
    }

    public virtual int GetIdleSound()
    {
        if (BaseSoundID != 0)
        {
            return BaseSoundID + 1;
        }

        return -1;
    }

    public virtual int GetAttackSound()
    {
        if (BaseSoundID != 0)
        {
            return BaseSoundID + 2;
        }

        return -1;
    }

    public virtual int GetHurtSound()
    {
        if (BaseSoundID != 0)
        {
            return BaseSoundID + 3;
        }

        return -1;
    }

    public virtual int GetDeathSound()
    {
        if (BaseSoundID != 0)
        {
            return BaseSoundID + 4;
        }

        if (m_Body.IsHuman)
        {
            return Utility.Random(m_Female ? 0x314 : 0x423, m_Female ? 4 : 5);
        }

        return -1;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ItemAtEnumerable<Item> GetItemsAt() =>
        m_Map == null ? Map.ItemAtEnumerable<Item>.Empty : m_Map.GetItemsAt(m_Location);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ItemAtEnumerable<T> GetItemsAt<T>() where T : Item =>
        m_Map == null ? Map.ItemAtEnumerable<T>.Empty : m_Map.GetItemsAt<T>(m_Location);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ItemBoundsEnumerable<Item> GetItemsInRange(int range) => GetItemsInRange<Item>(range);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ItemBoundsEnumerable<T> GetItemsInRange<T>(int range) where T : Item =>
        m_Map == null ? Map.ItemBoundsEnumerable<T>.Empty : m_Map.GetItemsInRange<T>(m_Location, range);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.MobileAtEnumerable<Mobile> GetMobilesInRange() => GetMobilesInRange<Mobile>();

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.MobileAtEnumerable<T> GetMobilesInRange<T>() where T : Mobile =>
        m_Map == null ? Map.MobileAtEnumerable<T>.Empty : m_Map.GetMobilesAt<T>(m_Location);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.MobileBoundsEnumerable<Mobile> GetMobilesInRange(int range) => GetMobilesInRange<Mobile>(range);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.MobileBoundsEnumerable<T> GetMobilesInRange<T>(int range) where T : Mobile =>
        m_Map == null ? Map.MobileBoundsEnumerable<T>.Empty : m_Map.GetMobilesInRange<T>(m_Location, range);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ClientAtEnumerable GetClientsAt() => m_Map == null ? Map.ClientAtEnumerable.Empty : Map.GetClientsAt(m_Location);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Map.ClientBoundsEnumerable GetClientsInRange(int range) =>
        m_Map == null ? Map.ClientBoundsEnumerable.Empty : Map.GetClientsInRange(m_Location, range);

    public void SayTo(Mobile to, bool ascii, string text) =>
        PrivateOverheadMessage(MessageType.Regular, SpeechHue, ascii, text, to.NetState);

    public void SayTo(Mobile to, string text) => SayTo(to, false, text);

    public void SayTo(Mobile to, int number, string args = "") =>
        to.NetState.SendMessageLocalized(Serial, Body, MessageType.Regular, SpeechHue, 3, number, Name, args);

    public void Say(bool ascii, string text) => PublicOverheadMessage(MessageType.Regular, SpeechHue, ascii, text);

    public void Say(string text) => PublicOverheadMessage(MessageType.Regular, SpeechHue, false, text);

    public void Say(int number, AffixType type, string affix, string args) =>
        PublicOverheadMessage(MessageType.Regular, SpeechHue, number, type, affix, args);

    public void Say(int number, string args = "") => PublicOverheadMessage(MessageType.Regular, SpeechHue, number, args);

    public void Emote(string text) => PublicOverheadMessage(MessageType.Emote, EmoteHue, false, text);

    public void Emote(int number, string args = "") => PublicOverheadMessage(MessageType.Emote, EmoteHue, number, args);

    public void Whisper(string text) => PublicOverheadMessage(MessageType.Whisper, WhisperHue, false, text);

    public void Whisper(int number, string args = "") =>
        PublicOverheadMessage(MessageType.Whisper, WhisperHue, number, args);

    public void Yell(string text) => PublicOverheadMessage(MessageType.Yell, YellHue, false, text);

    public void Yell(int number, string args = "") =>
        PublicOverheadMessage(MessageType.Yell, YellHue, number, args);

    public bool SendHuePicker(HuePicker p)
    {
        if (m_NetState != null)
        {
            p.SendTo(m_NetState);
            return true;
        }

        return false;
    }

    public bool SendMenu(IMenu m)
    {
        if (m_NetState == null)
        {
            return false;
        }

        m.SendTo(m_NetState);
        return true;
    }

    public virtual bool CanBeBeneficial(Mobile target) => CanBeBeneficial(target, true, false);

    public virtual bool CanBeBeneficial(Mobile target, bool message) => CanBeBeneficial(target, message, false);

    public virtual bool CanBeBeneficial(Mobile target, bool message, bool allowDead)
    {
        if (target == null)
        {
            return false;
        }

        if (Deleted || target.Deleted || !Alive || IsDeadBondedPet ||
            !allowDead && (!target.Alive || target.IsDeadBondedPet))
        {
            if (message)
            {
                SendLocalizedMessage(1001017); // You can not perform beneficial acts on your target.
            }

            return false;
        }

        if (target == this)
        {
            return true;
        }

        if ( /*m_Player &&*/!Region.AllowBeneficial(this, target))
        {
            // TODO: Pets
            // if (!(target.m_Player || target.Body.IsHuman || target.Body.IsAnimal))
            // {
            if (message)
            {
                SendLocalizedMessage(1001017); // You can not perform beneficial acts on your target.
            }

            return false;
            // }
        }

        return true;
    }

    public virtual bool IsBeneficialCriminal(Mobile target)
    {
        if (this == target)
        {
            return false;
        }

        var n = Notoriety.Compute(this, target);

        return n is Notoriety.Criminal or Notoriety.Murderer;
    }

    /// <summary>
    ///     Overridable. Event invoked when the Mobile <see cref="DoBeneficial">does a beneficial action</see>.
    /// </summary>
    public virtual void OnBeneficialAction(Mobile target, bool isCriminal)
    {
        if (isCriminal)
        {
            CriminalAction(false);
        }
    }

    public virtual void DoBeneficial(Mobile target)
    {
        if (target == null)
        {
            return;
        }

        OnBeneficialAction(target, IsBeneficialCriminal(target));

        Region.OnBeneficialAction(this, target);
        target.Region.OnGotBeneficialAction(this, target);
    }

    public virtual bool BeneficialCheck(Mobile target)
    {
        if (CanBeBeneficial(target, true))
        {
            DoBeneficial(target);
            return true;
        }

        return false;
    }

    public virtual bool CanBeHarmful(Mobile target) => CanBeHarmful(target, true);

    public virtual bool CanBeHarmful(Mobile target, bool message) => CanBeHarmful(target, message, false);

    public virtual bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
    {
        if (target == null)
        {
            return false;
        }

        if (Deleted || !ignoreOurBlessedness && m_Blessed || target.Deleted || target.m_Blessed || !Alive ||
            IsDeadBondedPet || !target.Alive || target.IsDeadBondedPet)
        {
            if (message)
            {
                SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
            }

            return false;
        }

        if (target == this)
        {
            return true;
        }

        // TODO: Pets
        if (!Region.AllowHarmful(this, target))
        {
            if (message)
            {
                SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
            }

            return false;
        }

        return true;
    }

    public virtual bool IsHarmfulCriminal(Mobile target) =>
        this != target && Notoriety.Compute(this, target) == Notoriety.Innocent;

    /// <summary>
    ///     Overridable. Event invoked when the Mobile <see cref="DoHarmful">does a harmful action</see>.
    /// </summary>
    public virtual void OnHarmfulAction(Mobile target, bool isCriminal)
    {
        if (isCriminal)
        {
            CriminalAction(false);
        }
    }

    public virtual void DoHarmful(Mobile target, bool indirect = false)
    {
        if (target == null || Deleted)
        {
            return;
        }

        var isCriminal = IsHarmfulCriminal(target);

        OnHarmfulAction(target, isCriminal);
        target.AggressiveAction(this, isCriminal);

        Region.OnDidHarmful(this, target);
        target.Region.OnGotHarmful(this, target);

        if (!indirect)
        {
            Combatant = target;
        }

        _expireCombatantTimerToken.Cancel();
        Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
    }

    public virtual bool HarmfulCheck(Mobile target)
    {
        if (CanBeHarmful(target))
        {
            DoHarmful(target);
            return true;
        }

        return false;
    }

    public virtual void RemoveStatMod(StatMod mod)
    {
        if (mod == null)
        {
            return;
        }

        // Remove it just in case it was orphaned somehow.
        mod.Remove();

        if (_statMods == null)
        {
            return;
        }

        if (_statMods.Remove(mod))
        {
            CheckStatTimers();
            Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
        }

        if (_statMods.Count == 0)
        {
            _statMods = null;
        }
    }

    public virtual void RemoveStatMod(string name)
    {
        if (_statMods == null || name == null)
        {
            return;
        }

        for (var i = _statMods.Count - 1; i >= 0; i--)
        {
            var mod = _statMods[i];
            if (mod.Name == name)
            {
                _statMods.RemoveAt(i);
                mod.Remove();
                CheckStatTimers();
                Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
            }
        }

        if (_statMods.Count == 0)
        {
            _statMods = null;
        }
    }

    public virtual StatMod GetStatMod(string name, bool includeElapsed = false)
    {
        if (_statMods == null || name == null)
        {
            return null;
        }

        for (var i = 0; i < _statMods.Count; i++)
        {
            var mod = _statMods[i];
            if (mod.Name == name && !mod.HasElapsed() || includeElapsed)
            {
                return mod;
            }
        }

        return null;
    }

    public virtual void AddStatMod(StatMod mod)
    {
        if (mod == null)
        {
            return;
        }

        mod.Owner = this;

        _statMods ??= new List<StatMod>();
        _statMods.Add(mod);
        Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
        CheckStatTimers();
    }

    private static MobileDelta GetStatDelta(StatType type)
    {
        MobileDelta delta = 0;

        if ((type & StatType.Str) != 0)
        {
            delta |= MobileDelta.Hits;
        }

        if ((type & StatType.Dex) != 0)
        {
            delta |= MobileDelta.Stam;
        }

        if ((type & StatType.Int) != 0)
        {
            delta |= MobileDelta.Mana;
        }

        return delta;
    }

    /// <summary>
    ///     Computes the total modified offset for the specified stat type.
    /// </summary>
    public int GetStatOffset(StatType type)
    {
        if (_statMods == null || _statMods.Count == 0)
        {
            return 0;
        }

        var offset = 0;

        for (var i = 0; i < _statMods.Count; i++)
        {
            var mod = _statMods[i];
            if ((mod.Type & type) != 0 && !mod.HasElapsed())
            {
                offset += mod.Offset;
            }
        }

        return offset;
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the <see cref="RawStr" /> changes.
    ///     <seealso cref="RawStr" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    public virtual void OnRawStrChange(int oldValue)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when <see cref="RawDex" /> changes.
    ///     <seealso cref="RawDex" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    public virtual void OnRawDexChange(int oldValue)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the <see cref="RawInt" /> changes.
    ///     <seealso cref="RawInt" />
    ///     <seealso cref="OnRawStatChange" />
    /// </summary>
    public virtual void OnRawIntChange(int oldValue)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the <see cref="RawStr" />, <see cref="RawDex" />, or <see cref="RawInt" />
    ///     changes.
    ///     <seealso cref="OnRawStrChange" />
    ///     <seealso cref="OnRawDexChange" />
    ///     <seealso cref="OnRawIntChange" />
    /// </summary>
    public virtual void OnRawStatChange(StatType stat, int oldValue)
    {
    }

    public virtual void OnHitsChange(int oldValue)
    {
    }

    public virtual void OnStamChange(int oldValue)
    {
    }

    public virtual void OnManaChange(int oldValue)
    {
    }

    /// <summary>
    ///     Overridable. Event invoked when a call to <see cref="ApplyPoison" /> failed because <see cref="CheckPoisonImmunity" />
    ///     returned false: the Mobile was resistant to the poison. By default, this broadcasts an overhead message: * The poison
    ///     seems to have no effect. *
    ///     <seealso cref="CheckPoisonImmunity" />
    ///     <seealso cref="ApplyPoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual void OnPoisonImmunity(Mobile from, Poison poison)
    {
        PublicOverheadMessage(MessageType.Emote, 0x3B2, 1005534); // * The poison seems to have no effect. *
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when a call to <see cref="ApplyPoison" /> failed because
    ///     <see cref="CheckHigherPoison" /> returned false: the Mobile was already poisoned by an equal or greater strength poison.
    ///     <seealso cref="CheckHigherPoison" />
    ///     <seealso cref="ApplyPoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual void OnHigherPoison(Mobile from, Poison poison)
    {
    }

    /// <summary>
    ///     Overridable. Event invoked when a call to <see cref="ApplyPoison" /> succeeded. By default, this broadcasts an overhead
    ///     message varying by the level of the poison. Example: * Zippy begins to spasm uncontrollably. *
    ///     <seealso cref="ApplyPoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual void OnPoisoned(Mobile from, Poison poison, Poison oldPoison)
    {
        if (poison != null)
        {
            LocalOverheadMessage(MessageType.Regular, 0x21, 1042857 + poison.Level * 2);
            NonlocalOverheadMessage(MessageType.Regular, 0x21, 1042858 + poison.Level * 2, Name);
        }
    }

    /// <summary>
    ///     Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is immune to some
    ///     <see cref="Poison" />. If true, <see cref="OnPoisonImmunity" /> will be invoked and
    ///     <see cref="ApplyPoisonResult.Immune" /> is returned.
    ///     <seealso cref="OnPoisonImmunity" />
    ///     <seealso cref="ApplyPoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual bool CheckPoisonImmunity(Mobile from, Poison poison) => false;

    /// <summary>
    ///     Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is already poisoned by some
    ///     <see cref="Poison" /> of equal or greater strength. If true, <see cref="OnHigherPoison" /> will be invoked and
    ///     <see cref="ApplyPoisonResult.HigherPoisonActive" /> is returned.
    ///     <seealso cref="OnHigherPoison" />
    ///     <seealso cref="ApplyPoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual bool CheckHigherPoison(Mobile from, Poison poison) =>
        m_Poison != null && m_Poison.Level >= poison.Level;

    /// <summary>
    ///     Overridable. Attempts to apply poison to the Mobile. Checks are made such that no
    ///     <see cref="CheckHigherPoison">higher poison is active</see> and that the Mobile is not
    ///     <see cref="CheckPoisonImmunity">immune to the poison</see>. Provided those assertions are true, the
    ///     <paramref name="poison" /> is applied and <see cref="OnPoisoned" /> is invoked.
    ///     <seealso cref="Poison" />
    ///     <seealso cref="CurePoison" />
    /// </summary>
    /// <returns>
    ///     One of four possible values:
    ///     <list type="table">
    ///         <item>
    ///             <term>
    ///                 <see cref="ApplyPoisonResult.Cured">Cured</see>
    ///             </term>
    ///             <description>The <paramref name="poison" /> parameter was null and so <see cref="CurePoison" /> was invoked.</description>
    ///         </item>
    ///         <item>
    ///             <term>
    ///                 <see cref="ApplyPoisonResult.HigherPoisonActive">HigherPoisonActive</see>
    ///             </term>
    ///             <description>The call to <see cref="CheckHigherPoison" /> returned false.</description>
    ///         </item>
    ///         <item>
    ///             <term>
    ///                 <see cref="ApplyPoisonResult.Immune">Immune</see>
    ///             </term>
    ///             <description>The call to <see cref="CheckPoisonImmunity" /> returned false.</description>
    ///         </item>
    ///         <item>
    ///             <term>
    ///                 <see cref="ApplyPoisonResult.Poisoned">Poisoned</see>
    ///             </term>
    ///             <description>The <paramref name="poison" /> was successfully applied.</description>
    ///         </item>
    ///     </list>
    /// </returns>
    public virtual ApplyPoisonResult ApplyPoison(Mobile from, Poison poison)
    {
        if (poison == null)
        {
            CurePoison(from);
            return ApplyPoisonResult.Cured;
        }

        if (CheckHigherPoison(from, poison))
        {
            OnHigherPoison(from, poison);
            return ApplyPoisonResult.HigherPoisonActive;
        }

        if (CheckPoisonImmunity(from, poison))
        {
            OnPoisonImmunity(from, poison);
            return ApplyPoisonResult.Immune;
        }

        var oldPoison = m_Poison;
        Poison = poison;

        OnPoisoned(from, poison, oldPoison);

        return ApplyPoisonResult.Poisoned;
    }

    /// <summary>
    ///     Overridable. Called from <see cref="CurePoison" />, this method checks to see that the Mobile can be cured of
    ///     <see cref="Poison" />
    ///     <seealso cref="CurePoison" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual bool CheckCure(Mobile from) => true;

    /// <summary>
    ///     Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> succeeded.
    ///     <seealso cref="CurePoison" />
    ///     <seealso cref="CheckCure" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual void OnCured(Mobile from, Poison oldPoison)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> failed.
    ///     <seealso cref="CurePoison" />
    ///     <seealso cref="CheckCure" />
    ///     <seealso cref="Poison" />
    /// </summary>
    public virtual void OnFailedCure(Mobile from)
    {
    }

    /// <summary>
    ///     Overridable. Attempts to cure any poison that is currently active.
    /// </summary>
    /// <returns>True if poison was cured, false if otherwise.</returns>
    public virtual bool CurePoison(Mobile from)
    {
        if (CheckCure(from))
        {
            var oldPoison = m_Poison;
            Poison = null;

            OnCured(from, oldPoison);

            return true;
        }

        OnFailedCure(from);

        return false;
    }

    public void MovingEffect(
        IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
        int hue, int renderMode
    ) =>
        Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes, hue, renderMode);

    public void MovingEffect(IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes) =>
        Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes);

    public void MovingParticles(
        IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
        int hue, int renderMode, int effect, int explodeEffect, int explodeSound, EffectLayer layer, int unknown
    ) =>
        Effects.SendMovingParticles(
            this,
            to,
            itemID,
            speed,
            duration,
            fixedDirection,
            explodes,
            hue,
            renderMode,
            effect,
            explodeEffect,
            explodeSound,
            layer,
            unknown
        );

    public void MovingParticles(
        IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
        int hue, int renderMode, int effect, int explodeEffect, int explodeSound, int unknown
    ) =>
        Effects.SendMovingParticles(
            this,
            to,
            itemID,
            speed,
            duration,
            fixedDirection,
            explodes,
            hue,
            renderMode,
            effect,
            explodeEffect,
            explodeSound,
            (EffectLayer)255,
            unknown
        );

    public void MovingParticles(
        IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
        int effect, int explodeEffect, int explodeSound, int unknown
    ) =>
        Effects.SendMovingParticles(
            this,
            to,
            itemID,
            speed,
            duration,
            fixedDirection,
            explodes,
            effect,
            explodeEffect,
            explodeSound,
            unknown
        );

    public void MovingParticles(
        IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
        int effect, int explodeEffect, int explodeSound
    ) =>
        Effects.SendMovingParticles(
            this,
            to,
            itemID,
            speed,
            duration,
            fixedDirection,
            explodes,
            0,
            0,
            effect,
            explodeEffect,
            explodeSound,
            0
        );

    public void FixedEffect(int itemID, int speed, int duration, int hue, int renderMode) =>
        Effects.SendTargetEffect(this, itemID, speed, duration, hue, renderMode);

    public void FixedEffect(int itemID, int speed, int duration) =>
        Effects.SendTargetEffect(this, itemID, speed, duration);

    public void FixedParticles(
        int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer, int unknown
    ) =>
        Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer, unknown);

    public void FixedParticles(
        int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer
    ) =>
        Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer);

    public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer, int unknown) =>
        Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer, unknown);

    public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer) =>
        Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer);

    public void BoltEffect(int hue) => Effects.SendBoltEffect(this, true, hue);

    public Direction GetDirectionTo(int x, int y, bool run = false)
    {
        var dx = m_Location.m_X - x;
        var dy = m_Location.m_Y - y;

        var rx = (dx - dy) * 44;
        var ry = (dx + dy) * 44;

        var ax = rx.Abs();
        var ay = ry.Abs();

        Direction ret;

        if ((ay >> 1) - ax >= 0)
        {
            ret = ry > 0 ? Direction.Up : Direction.Down;
        }
        else if ((ax >> 1) - ay >= 0)
        {
            ret = rx > 0 ? Direction.Left : Direction.Right;
        }
        else
        {
            ret = rx switch
            {
                >= 0 when ry >= 0 => Direction.West,
                >= 0              => Direction.South,
                < 0 when ry < 0   => Direction.East,
                _                 => Direction.North
            };
        }

        if (run)
        {
            ret |= Direction.Running;
        }

        return ret;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Direction GetDirectionTo(Point2D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Direction GetDirectionTo(Point3D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public Direction GetDirectionTo(IPoint2D p, bool run = false) =>
        p == null ? Direction.North | (run ? Direction.Running : 0) : GetDirectionTo(p.X, p.Y, run);

    public void PublicOverheadMessage(
        MessageType type, int hue, bool ascii, string text, bool noLineOfSight = true,
        AccessLevel accessLevel = AccessLevel.Player
    )
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (
                state.Mobile.AccessLevel >= accessLevel &&
                state.Mobile.CanSee(this) &&
                (noLineOfSight || state.Mobile.InLOS(this))
            )
            {
                var length = OutgoingMessagePackets.CreateMessage(
                    buffer, Serial, Body, type, hue, 3, ascii, Language, Name, text
                );

                if (length != buffer.Length)
                {
                    buffer = buffer[..length]; // Adjust to the actual size
                }

                state.Send(buffer);
            }
        }
    }

    public void PublicOverheadMessage(MessageType type, int hue, int number, string args = "", bool noLineOfSight = true)
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedLength(args)].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (state.Mobile.CanSee(this) && (noLineOfSight || state.Mobile.InLOS(this)))
            {
                var length = OutgoingMessagePackets.CreateMessageLocalized(
                    buffer, Serial, Body, type, hue, 3, number, Name, args
                );

                if (length != buffer.Length)
                {
                    buffer = buffer[..length]; // Adjust to the actual size
                }

                state.Send(buffer);
            }
        }
    }

    public void PublicOverheadMessage(
        MessageType type, int hue, int number, AffixType affixType, string affix,
        string args = "", bool noLineOfSight = false,
        AccessLevel accessLevel = AccessLevel.Player
    )
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedAffixLength(affix, args)]
            .InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (
                state.Mobile.AccessLevel >= accessLevel &&
                state.Mobile.CanSee(this) &&
                (noLineOfSight || state.Mobile.InLOS(this))
            )
            {
                var length = OutgoingMessagePackets.CreateMessageLocalizedAffix(
                    buffer, Serial, Body, type, hue, 3, number, Name, affixType, affix, args
                );

                if (length != buffer.Length)
                {
                    buffer = buffer[..length]; // Adjust to the actual size
                }

                state.Send(buffer);
            }
        }
    }

    public void PrivateOverheadMessage(MessageType type, int hue, bool ascii, string text, NetState state)
    {
        state.SendMessage(Serial, Body, type, hue, 3, ascii, m_Language, Name, text);
    }

    public void PrivateOverheadMessage(MessageType type, int hue, int number, NetState state) =>
        PrivateOverheadMessage(type, hue, number, "", state);

    public void PrivateOverheadMessage(MessageType type, int hue, int number, string args, NetState state) =>
        state.SendMessageLocalized(Serial, Body, type, hue, 3, number, Name, args);

    public void LocalOverheadMessage(MessageType type, int hue, bool ascii, string text) =>
        m_NetState.SendMessage(Serial, Body, type, hue, 3, ascii, m_Language, Name, text);

    public void LocalOverheadMessage(MessageType type, int hue, int number, string args = "") =>
        m_NetState.SendMessageLocalized(Serial, Body, type, hue, 3, number, Name, args);

    public void NonlocalOverheadMessage(MessageType type, int hue, int number, string args = "")
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLocalizedLength(args)].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (state != m_NetState && state.Mobile.CanSee(this))
            {
                var length = OutgoingMessagePackets.CreateMessageLocalized(
                    buffer, Serial, Body, type, hue, 3, number, Name, args
                );

                if (length != buffer.Length)
                {
                    buffer = buffer[..length]; // Adjust to the actual size
                }

                state.Send(buffer);
            }
        }
    }

    public void NonlocalOverheadMessage(MessageType type, int hue, bool ascii, string text)
    {
        if (m_Map == null)
        {
            return;
        }

        Span<byte> buffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text)].InitializePacket();

        foreach (var state in m_Map.GetClientsInRange(m_Location))
        {
            if (state != m_NetState && state.Mobile.CanSee(this))
            {
                var length = OutgoingMessagePackets.CreateMessage(
                    buffer, Serial, Body, type, hue, 3, ascii, Language, Name, text
                );

                if (length != buffer.Length)
                {
                    buffer = buffer[..length]; // Adjust to the actual size
                }

                state.Send(buffer);
            }
        }
    }

    public void SendLocalizedMessage(int number, string args = "", int hue = 0x3B2) =>
        m_NetState.SendMessageLocalized(Serial.MinusOne, -1, MessageType.Regular, hue, 3, number, "System", args);

    public void SendLocalizedMessage(int number, bool append, string affix, string args = "", int hue = 0x3B2) =>
        m_NetState.SendMessageLocalizedAffix(
            Serial.MinusOne,
            -1,
            MessageType.Regular,
            hue,
            3,
            number,
            "System",
            (append ? AffixType.Append : AffixType.Prepend) | AffixType.System,
            affix,
            args
        );

    public void SendMessage(string text) => SendMessage(0x3B2, text);

    public void SendMessage(int hue, string text) =>
        m_NetState.SendMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, false, "ENU", "System", text);

    public void SendAsciiMessage(string text) => SendAsciiMessage(0x3B2, text);

    public void SendAsciiMessage(int hue, string text) =>
        m_NetState.SendMessage(Serial.MinusOne, -1, MessageType.Regular, hue, 3, true, null, "System", text);

    /// <summary>
    ///     Overridable. Event invoked when the Mobile is double clicked. By default, this method can either dismount or open the
    ///     paperdoll.
    ///     <seealso cref="CanPaperdollBeOpenedBy" />
    ///     <seealso cref="DisplayPaperdollTo" />
    /// </summary>
    public virtual void OnDoubleClick(Mobile from)
    {
        if (this == from && (!DisableDismountInWarmode || !m_Warmode))
        {
            var mount = Mount;

            if (mount != null)
            {
                mount.Rider = null;
                return;
            }
        }

        if (CanPaperdollBeOpenedBy(from))
        {
            DisplayPaperdollTo(from);
        }
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile is double clicked by someone who is over 18 tiles away.
    ///     <seealso cref="OnDoubleClick" />
    /// </summary>
    public virtual void OnDoubleClickOutOfRange(Mobile from)
    {
    }

    /// <summary>
    ///     Overridable. Virtual event invoked when the Mobile is double clicked by someone who can no longer see the Mobile. This
    ///     may
    ///     happen, for example, using 'Last Object' after the Mobile has hidden.
    ///     <seealso cref="OnDoubleClick" />
    /// </summary>
    public virtual void OnDoubleClickCantSee(Mobile from)
    {
    }

    /// <summary>
    ///     Overridable. Event invoked when the Mobile is double clicked by someone who is not alive. Similar to
    ///     <see cref="OnDoubleClick" />, this method will show the paperdoll. It does not, however, provide any dismount
    ///     functionality.
    ///     <seealso cref="OnDoubleClick" />
    /// </summary>
    public virtual void OnDoubleClickDead(Mobile from)
    {
        if (CanPaperdollBeOpenedBy(from))
        {
            DisplayPaperdollTo(from);
        }
    }

    private class ManaTimer : Timer
    {
        private readonly Mobile m_Owner;

        public ManaTimer(Mobile m) : base(GetManaRegenRate(m), GetManaRegenRate(m)) => m_Owner = m;

        protected override void OnTick()
        {
            if (m_Owner.CanRegenMana)
            {
                m_Owner.Mana++;
            }

            Delay = Interval = GetManaRegenRate(m_Owner);
        }
    }

    private class HitsTimer : Timer
    {
        private readonly Mobile m_Owner;

        public HitsTimer(Mobile m) : base(GetHitsRegenRate(m), GetHitsRegenRate(m)) => m_Owner = m;

        protected override void OnTick()
        {
            if (m_Owner.CanRegenHits)
            {
                m_Owner.Hits++;
            }

            Delay = Interval = GetHitsRegenRate(m_Owner);
        }
    }

    private class StamTimer : Timer
    {
        private readonly Mobile m_Owner;

        public StamTimer(Mobile m) : base(GetStamRegenRate(m), GetStamRegenRate(m)) => m_Owner = m;

        protected override void OnTick()
        {
            if (m_Owner.CanRegenStam)
            {
                m_Owner.Stam++;
            }

            Delay = Interval = GetStamRegenRate(m_Owner);
        }
    }

    private void Logout()
    {
        if (m_Map != Map.Internal)
        {
            EventSink.InvokeLogout(this);

            LogoutLocation = m_Location;
            LogoutMap = m_Map;

            Internalize();
        }
    }

    private void ExpireCriminal()
    {
        Criminal = false;
    }
}
