/************************************************************************* * ModernUO * * Copyright 2019-2026 - 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 . * *************************************************************************/ 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 System; using System.Diagnostics; using System.Collections.Generic; using System.Runtime.CompilerServices; using Server.Buffers; using CalcMoves = Server.Movement.Movement; namespace Server; public delegate void TargetCallback(Mobile from, object targeted); public delegate void TargetStateCallback(Mobile from, object targeted, T state); public delegate void PromptCallback(Mobile from, string text); public delegate void PromptStateCallback(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 Responsible { get; set; } public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0); } [Flags] public enum StatType : byte { 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 initialContent, List equippedItems ); public delegate int AOSStatusHandler(Mobile from, int index); /// /// Base class representing players, npcs, and creatures. /// public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode { // 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 m_Hears = new(); private static readonly List 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 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 _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 int _facialHairItemId; private int _facialHairHue; private Serial _facialHairSerial; 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 int _hairItemId; private int _hairHue; private Serial _hairSerial; 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 int _thirst; 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 _statMods; private List _resistanceMods; private List _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(); Aggressed = new List(); NextSkillTime = Core.TickCount; DamageEntries = new List(); } // 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 SkillMods => _skillMods; public List StatMods => _statMods; public List 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 => _thirst; set { var oldValue = _thirst; if (oldValue != value) { _thirst = value; OnThirstChanged(oldValue); } } } [CommandProperty(AccessLevel.GameMaster)] public int BAC { get; set; } /// /// Gets or sets the number of steps this player may take when hidden before being revealed. /// [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(); } } } /// /// Gets or sets the lock state for the property. /// [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public StatLockType StrLock { get => m_StrLock; set { if (m_StrLock != value) { m_StrLock = value; m_NetState.SendStatLockInfo(this); } } } /// /// Gets or sets the lock state for the property. /// [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public StatLockType DexLock { get => m_DexLock; set { if (m_DexLock != value) { m_DexLock = value; m_NetState.SendStatLockInfo(this); } } } /// /// Gets or sets the lock state for the property. /// [CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)] public StatLockType IntLock { get => m_IntLock; set { if (m_IntLock != value) { m_IntLock = value; m_NetState.SendStatLockInfo(this); } } } [CommandProperty(AccessLevel.Administrator)] 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; [CommandProperty(AccessLevel.Administrator)] public long NextSkillTime { get; set; } public List Aggressors { get; private set; } public List Aggressed { get; private set; } public bool ChangingCombatant => m_ChangingCombatant > 0; private void ExpireCombatant() { Combatant = null; _expireCombatantTimerToken.Cancel(); } /// /// Overridable. Gets or sets which Mobile that this Mobile is currently engaged in combat with. /// /// [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.Administrator, canModify: true)] 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; /// /// Overridable. Returns true if the player is alive, false if otherwise. By default, this is computed by: /// !Deleted && (!Player || !Body.IsGhost) /// [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 int ActionDelay { get; set; } = 500; public static VisibleDamageType VisibleDamageType { get; set; } public List 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 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 { return m_NetState is { IsConnected: true } ? m_NetState : null; } 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(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(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 != Murderer) { Delta(MobileDelta.Noto); InvalidateProperties(); } OnKillsChange(oldValue); } } } public virtual bool Murderer => Kills >= 5; [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; /// /// Gets or sets the maximum attainable value for , , and . /// [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); } } /// /// Gets or sets the base, unmodified, strength of the Mobile. Ranges from 1 to 65000, inclusive. /// /// /// /// /// [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); } } } /// /// Gets or sets the effective strength of the Mobile. This is the sum of the plus any additional /// modifiers. Any attempts to set this value when under the influence of a will result in no change. /// It ranges from 1 to 65000, inclusive. /// /// /// [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; } } } /// /// Gets or sets the base, unmodified, dexterity of the Mobile. Ranges from 1 to 65000, inclusive. /// /// /// /// /// [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); } } } /// /// Gets or sets the effective dexterity of the Mobile. This is the sum of the plus any additional /// modifiers. Any attempts to set this value when under the influence of a will result in no change. /// It ranges from 1 to 65000, inclusive. /// /// /// [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; } } } /// /// Gets or sets the base, unmodified, intelligence of the Mobile. Ranges from 1 to 65000, inclusive. /// /// /// /// /// [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); } } } /// /// Gets or sets the effective intelligence of the Mobile. This is the sum of the plus any additional /// modifiers. Any attempts to set this value when under the influence of a will result in no change. /// It ranges from 1 to 65000, inclusive. /// /// /// [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; } } } /// /// Gets or sets the current hit point of the Mobile. This value ranges from 0 to , inclusive. When /// set /// to the value of , the CanReportMurder flag of all /// aggressors is reset to false, and the list of damage entries is cleared. /// [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); } } } /// /// Overridable. Gets the maximum hit point of the Mobile. By default, this returns: 50 + ( / 2) /// [CommandProperty(AccessLevel.GameMaster)] public virtual int HitsMax => 50 + Str / 2; /// /// Gets or sets the current stamina of the Mobile. This value ranges from 0 to , inclusive. /// [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); } } } /// /// Overridable. Gets the maximum stamina of the Mobile. By default, this returns: /// /// /// /// [CommandProperty(AccessLevel.GameMaster)] public virtual int StamMax => Dex; /// /// Gets or sets the current stamina of the Mobile. This value ranges from 0 to , inclusive. /// [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); } } } /// /// Overridable. Gets the maximum mana of the Mobile. By default, this returns: /// /// /// /// [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 => _hairItemId; set { // An item id of 0 is the "no hair" state; clear the hue with it, but keep // _hairSerial so a pending removal packet references the equipped serial. _hairItemId = value < 0 ? 0 : value; if (_hairItemId == 0) { _hairHue = 0; } Delta(MobileDelta.Hair); this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public int FacialHairItemID { get => _facialHairItemId; set { _facialHairItemId = value < 0 ? 0 : value; if (_facialHairItemId == 0) { _facialHairHue = 0; } Delta(MobileDelta.FacialHair); this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public int HairHue { get => _hairHue; set { if (_hairItemId > 0) { _hairHue = value; Delta(MobileDelta.Hair); this.MarkDirty(); } } } [CommandProperty(AccessLevel.GameMaster)] public int FacialHairHue { get => _facialHairHue; set { if (_facialHairItemId > 0) { _facialHairHue = value; Delta(MobileDelta.FacialHair); this.MarkDirty(); } } } public Serial HairSerial { get { if (_hairItemId > 0 && _hairSerial == Serial.Zero) { _hairSerial = World.NewVirtual; } return _hairSerial; } } public Serial FacialHairSerial { get { if (_facialHairItemId > 0 && _facialHairSerial == Serial.Zero) { _facialHairSerial = World.NewVirtual; } return _facialHairSerial; } } 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 { get { if (m_PropertyList == null) { // Publish the list before building it so a nested InvalidateProperties can see the // build in progress and defer instead of recursing into a second throwaway list. var list = new ObjectPropertyList(this); m_PropertyList = list; InitializePropertyList(list); } return m_PropertyList; } } 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(38); // version writer.WriteAnchoredTime(LastStrGain); writer.WriteAnchoredTime(LastIntGain); writer.WriteAnchoredTime(LastDexGain); byte hairflag = 0x00; if (_hairItemId > 0) { hairflag |= 0x01; } if (_facialHairItemId > 0) { hairflag |= 0x02; } writer.Write(hairflag); if ((hairflag & 0x01) != 0) { writer.Write(_hairItemId); writer.Write(_hairHue); } if ((hairflag & 0x02) != 0) { writer.Write(_facialHairItemId); writer.Write(_facialHairHue); } 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; m_MountItem = null; World.RemoveEntity(this); OnAfterDelete(); m_PropertyList = null; } public virtual bool SkipSerialization => false; [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.ResetMovementState(); 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 = HairSerial; var hairLength = removeHair ? OutgoingVirtualHairPackets.RemovePacketLength : OutgoingVirtualHairPackets.EquipUpdatePacketLength; var hairPacket = stackalloc byte[hairLength].InitializePacket(); var facialHairSerial = FacialHairSerial; var facialHairLength = removeFacialHair ? OutgoingVirtualHairPackets.RemovePacketLength : OutgoingVirtualHairPackets.EquipUpdatePacketLength; var facialHairPacket = stackalloc byte[facialHairLength].InitializePacket(); const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength; var mobileMovingCache = stackalloc byte[cacheLength].InitializePacket(); var ourState = m_NetState; if (ourState != null) { if (sendUpdate) { ourState.ResetMovementState(); 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; } var statBufferTrue = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket(); var statBufferFalse = stackalloc byte[OutgoingMobilePackets.MobileStatusCompactLength].InitializePacket(); var hbpBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket(); var hbyBuffer = stackalloc byte[OutgoingMobilePackets.MobileHealthbarPacketLength].InitializePacket(); var deadBuffer = stackalloc byte[OutgoingMobilePackets.BondedStatusPacketLength].InitializePacket(); var removeEntity = stackalloc byte[OutgoingEntityPackets.RemoveEntityLength].InitializePacket(); var hitsPacket = stackalloc byte[OutgoingMobilePackets.MobileAttributePacketLength].InitializePacket(); mobileMovingCache.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(); _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); var 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(); } /// /// Overridable. Virtual event invoked when changes in some way. /// 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(); _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; } } /// /// Overridable. Virtual event invoked when a client, , invokes a 'help request' for the Mobile. /// Seemingly no longer functional in newer clients. /// 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() => BeginAction(typeof(T)); public bool BeginAction(object toLock) { if (_actions == null) { _actions = new List { toLock }; return true; } if (!_actions.Contains(toLock)) { _actions.Add(toLock); return true; } return false; } public bool CanBeginAction() => CanBeginAction(typeof(T)); public bool CanBeginAction(object toLock) => _actions?.Contains(toLock) != true; public void EndAction() => 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); /// /// Overridable. Virtual event invoked after the property has changed. /// /// public virtual void OnCombatantChange() { } /// /// Overridable. Virtual event invoked after the property has changed. /// /// public virtual void OnHungerChanged(int oldValue) { } /// /// Overridable. Virtual event invoked after the property has changed. /// /// public virtual void OnThirstChanged(int oldValue) { } 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( int range, bool allowGround, TargetFlags flags, TargetStateCallback callback, T state ) => Target = new SimpleStateTarget(range, flags, allowGround, callback, state); /// /// Overridable. Virtual event invoked after the Target property has changed. /// 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; } /// /// Overridable. Event invoked before the Mobile moves. /// /// True if the move is allowed, false if not. 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.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) { var cost = ComputeMovementSpeed(d); var now = Core.TickCount; var delta = now - m_NetState._nextMovementTime; // Cap drift to prevent banking "lateness" for speed hacking later. // maxDrift should match the credit buffer so the systems are symmetric. const int maxDrift = 200; if (cost == 0) { // Direction-only turn: reset to now to prevent turn accumulation m_NetState._nextMovementTime = now; } else { // Clamp _nextMovementTime so it's never more than maxDrift behind now. // This limits how much "lateness" can be banked. if (delta > maxDrift) { m_NetState._nextMovementTime = now - maxDrift; } // Accumulative timing: add cost to current baseline m_NetState._nextMovementTime += cost; } m_NetState.SendMovementAck(m_NetState.Sequence, this); } SetLocation(newLocation, false); SetDirection(d); if (m_Map != null) { using var moveQueue = PooledRefQueue.Create(2048); using var moveClientQueue = PooledRefQueue.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 mobileMovingCache = stackalloc byte[cacheLength]; mobileMovingCache.InitializePacket(); 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; } /// /// Overridable. Virtual event invoked when a Mobile moves off this Mobile. /// /// True if the move is allowed, false if not. public virtual bool OnMoveOff(Mobile m) => true; /// /// Overridable. Event invoked when a Mobile moves over this Mobile. /// /// True if the move is allowed, false if not. 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; } /// /// Overridable. Virtual event invoked when the Mobile sees another Mobile, , move. /// 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; /// /// Overridable. Any call to will silently fail if this method returns false. /// /// public virtual bool CheckResurrect() => true; /// /// Overridable. Event invoked before the Mobile is resurrected. /// /// public virtual void OnBeforeResurrect() { } /// /// Overridable. Event invoked after the Mobile is resurrected. /// /// 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(); } } #if TRACK_LEAKS ~Mobile() { EntityFinalizationTracker.NotifyFinalized(this); } #endif /// /// Overridable. Virtual event invoked before the Mobile is deleted. /// public virtual void OnDelete() { Spawner?.Remove(this); Spawner = null; } public virtual bool CheckSpellCast(ISpell spell) => true; /// /// Overridable. Virtual event invoked when the Mobile casts a . /// /// public virtual void OnSpellCast(ISpell spell) { var now = Core.TickCount; if (m_NetState != null && now - m_NetState._nextMovementTime > 0) { m_NetState._nextMovementTime = now; } } /// /// Overridable. Virtual event invoked after changes. /// public virtual void OnWeightChange(int oldValue) { } /// /// Overridable. Virtual event invoked when the or property of /// changes. /// public virtual void OnSkillChange(SkillName skill, double oldBase) { } /// /// Overridable. Invoked after the mobile is deleted. When overridden, be sure to call the base method. /// 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(); var equip = new List(); var moveToPack = new List(); var itemsCopy = new List(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(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; var deathAnimation = stackalloc byte[OutgoingMobilePackets.DeathAnimationPacketLength].InitializePacket(); var 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); } /// /// Overridable. Event invoked before the Mobile is killed. /// /// /// /// True to continue with death, false to override it. public virtual bool OnBeforeDeath() => true; /// /// Overridable. Event invoked after the Mobile is killed. Primarily, this method is responsible for /// deleting an NPC or turning a PC into a ghost. /// /// /// public virtual void OnDeath(Container c) { var sound = GetDeathSound(); if (sound >= 0) { Effects.PlaySound(this, sound); } if (!m_Player) { Delete(); return; } 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; 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; var 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) { // Don't re-show a duped-layer item on its own (see Item.IsDupedEquipLayer). if (!item.IsDupedEquipLayer()) { state.SendEquipUpdate(item); } } else { item.SendInfoTo(state); } if (item.Parent != null && !item.IsDupedEquipLayer()) { item.SendOPLPacketTo(state); } } } public static T LiftItemDupe(T oldItem, int amount) where T : Item { T item; try { item = oldItem.GetType().CreateInstance(); } 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.PlayerConstructed = oldItem.PlayerConstructed; item.Amount = oldAmount - amount; // A parented remainder gets its map from AddItem (parent first, then map), keeping the // split off the decay scheduler; a ground remainder is placed and enrolled here. if (oldItem.Parent == null) { 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; } var 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 hears, ref string text, ref object context) { if (Alive) { return false; } ReadOnlySpan ghostChars = (GhostChars ?? DefaultGhostChars).AsSpan(); var length = text.Length; char[] rentedChars = null; var chars = length <= 256 ? stackalloc char[length] : rentedChars = STArrayPool.Shared.Rent(length); try { var textSpan = text.AsSpan(); for (var i = 0; i < textSpan.Length; ++i) { chars[i] = textSpan[i] != ' ' ? ghostChars.RandomElement() : ' '; } text = new string(chars[..length]); } finally { if (rentedChars != null) { STArrayPool.Shared.Return(rentedChars); } } 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 static void AddSpeechItemsFrom(List 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: case MessageType.Label: case MessageType.Focus: case MessageType.Spell: case MessageType.Guild: case MessageType.Alliance: case MessageType.Command: 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()); } CheckSpeechManifest(); ProcessDelta(); var regBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(text.Length)].InitializePacket(); var mutBuffer = stackalloc byte[OutgoingMessagePackets.GetMaxMessageLength(mutatedText.Length)].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 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; } /// /// Overridable. Virtual event invoked when the Mobile is damaged. It is called before /// hit points are lowered or the Mobile is killed. /// /// /// /// 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 38: // Stat-gain stamps moved from delta time to anchored time case 37: // Decomposed hair into inline item id/hue (dropped the VirtualHairInfo object) 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: { if (version >= 38) { LastStrGain = reader.ReadAnchoredTime(); LastIntGain = reader.ReadAnchoredTime(); LastDexGain = reader.ReadAnchoredTime(); } else { LastStrGain = reader.ReadDeltaTime(); LastIntGain = reader.ReadDeltaTime(); LastDexGain = reader.ReadDeltaTime(); } goto case 30; } case 30: { // Before v37 each hair was a VirtualHairInfo whose Serialize wrote a // leading version int ahead of the item id and hue. var hairflag = reader.ReadByte(); if ((hairflag & 0x01) != 0) { if (version < 37) { reader.ReadInt(); // legacy VirtualHairInfo version } _hairItemId = reader.ReadInt(); _hairHue = reader.ReadInt(); } if ((hairflag & 0x02) != 0) { if (version < 37) { reader.ReadInt(); // legacy VirtualHairInfo version } _facialHairItemId = reader.ReadInt(); _facialHairHue = reader.ReadInt(); } 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(); 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(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(); goto case 12; } case 12: { m_Guild = reader.ReadEntity(); 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(); 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(); 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(); _skillMods = new List(); 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 list, Item item) { } public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList list) { } public void Internalize() { Map = Map.Internal; } /// /// Overridable. Virtual event invoked when is added from the Mobile, /// such /// as when it is equipped. /// /// /// public virtual void OnItemAdded(Item item) { } /// /// Overridable. Virtual event invoked when is removed from the /// Mobile. /// /// /// public virtual void OnItemRemoved(Item item) { } /// /// Overridable. Virtual event invoked when is becomes a child of the Mobile; it's worn or /// contained /// at some level of the Mobile's backpack or bank box /// /// /// public virtual void OnSubItemAdded(Item item) { } /// /// Overridable. Virtual event invoked when is removed from the Mobile, its /// backpack, or its bank box. /// /// /// 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(); var 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; } var 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; } var 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); } } } /// /// Overridable. Event invoked before the Mobile says something. /// /// 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; /// /// Overridable. Virtual event invoked when the Mobile hears speech. This event will only be invoked if /// returns true. /// /// 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); } } /// /// Overridable. Virtual event invoked when changes. /// 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() { } /// /// Overridable. Virtual event invoked after the Warmode property has changed. /// public virtual void OnWarmodeChanged() { } public virtual void OnHiddenChanged() { AllowedStealthSteps = 0; if (m_Map == null) { return; } var 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 is Item parent) { if (!(CanSee(parent) && parent.IsChildVisibleTo(this, item))) { return false; } } else if (item.Parent is Mobile mobile && !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) { list.IsBuilding = true; try { GetProperties(list); list.Terminate(); } finally { list.IsBuilding = false; } return list; } public void ClearProperties() { m_PropertyList = null; } #nullable enable public void InvalidateProperties() { if (!ObjectPropertyList.Enabled) { return; } // Always a bug in the property getter, and there is no correct recovery: refuse rather than // hide it. RELEASE keeps a possibly stale tooltip, DEBUG throws. // See dev-docs/property-lists.md "Never Invalidate From Inside GetProperties". if (m_PropertyList?.IsBuilding == true) { logger.Error( "{Entity} called InvalidateProperties() while its property list was being built. Remove the side effect from the property getter, or defer it with Timer.DelayCall.\n{StackTrace}", this, new StackTrace() ); #if DEBUG throw new InvalidOperationException( $"{this} invalidated its property list from inside GetProperties. Remove the side effect from the property getter." ); #else return; #endif } 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.ResetMovementState(); 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) var 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); } /// /// Overridable. Virtual event invoked when changes. /// 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(Layer.Bank); } return m_BankBox; } [MethodImpl(MethodImplOptions.AggressiveInlining)] public T FindItemOnLayer(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 (!ServerFeatureFlags.PlayerTrading) { from.SendMessage(0x22, "Player trading is temporarily disabled."); return false; } 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; } /// /// Overridable. Event invoked when a Mobile () drops an /// /// /// /// onto the Mobile. /// 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; } /// /// Overridable. Virtual event invoked when the Mobile attempts to wear . /// /// True if the request is accepted, false if otherwise. 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; } /// /// Overridable. Virtual event invoked when the Mobile attempts to lift . /// /// True if the lift is allowed, false if otherwise. /// /// The following example demonstrates usage. It will disallow any attempts to pick up a pick axe if the Mobile does not /// have /// enough strength. /// /// public override bool OnDragLift( Item item ) /// { /// if (item is Pickaxe && this.Str < 60) /// { /// SendMessage( "That is too heavy for you to lift." ); /// return false; /// } /// /// return base.OnDragLift( item ); /// } /// public virtual bool OnDragLift(Item item) => true; /// /// Overridable. Virtual event invoked when the Mobile attempts to drop into a /// /// /// /// . /// /// True if the drop is allowed, false if otherwise. public virtual bool OnDroppedItemInto(Item item, Container container, Point3D loc) => true; /// /// Overridable. Virtual event invoked when the Mobile attempts to drop directly onto another /// , . This is the case of stacking items. /// /// True if the drop is allowed, false if otherwise. public virtual bool OnDroppedItemOnto(Item item, Item target) => true; /// /// Overridable. Virtual event invoked when the Mobile attempts to drop into another /// , . The target item is most likely a . /// /// True if the drop is allowed, false if otherwise. public virtual bool OnDroppedItemToItem(Item item, Item target, Point3D loc) => true; /// /// Overridable. Virtual event invoked when the Mobile attempts to give to a Mobile ( /// ). /// /// True if the drop is allowed, false if otherwise. public virtual bool OnDroppedItemToMobile(Item item, Mobile target) => true; /// /// Overridable. Virtual event invoked when the Mobile attempts to drop to the world at a /// /// /// /// . /// /// True if the drop is allowed, false if otherwise. public virtual bool OnDroppedItemToWorld(Item item, Point3D location) => true; /// /// Overridable. Virtual event when successfully uses while it's on this /// Mobile. /// /// 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; /// /// Overridable. Virtual event invoked when successfully lifts from this /// Mobile. /// /// 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(); _statMods = new List(); _skillMods = new List(); Map = Map.Internal; AutoPageNotify = true; Aggressors = new List(); Aggressed = new List(); DamageEntries = new List(); 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); } } /// /// True when deltas remain queued after a pass, which is /// bounded by the count it saw on entry. The event loop consults this before sleeping. /// public static bool HasQueuedDeltas => m_DeltaQueue.Count > 0; 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(ReadOnlySpan 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); } /// /// Overridable. Event invoked when the Mobile requests to open his own paperdoll via the 'Open Paperdoll' macro. /// public virtual void OnPaperdollRequest() { if (CanPaperdollBeOpenedBy(this)) { DisplayPaperdollTo(this); } } /// /// Overridable. Event invoked when wants to see this Mobile's stats. /// /// 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); } } /// /// Overridable. Event invoked when wants to see this Mobile's skills. /// public virtual void OnSkillsQuery(Mobile from) { if (from == this) { m_NetState.SendSkillsUpdate(Skills); } } /// /// Overridable. Virtual event invoked when changes. /// public virtual void OnRegionChange(Region old, Region @new) { } /// /// Overridable. Event invoked when the Mobile is single clicked. /// 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 = ""; } if (title.Length <= 0) { PrivateOverheadMessage( MessageType.Regular, SpeechHue, true, $"[{guild.Abbreviation}]{type}", from.NetState ); } else { PrivateOverheadMessage( MessageType.Regular, SpeechHue, true, $"[{title}, {guild.Abbreviation}]{type}", 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. } } /// /// Overridable. Virtual event invoked when the sector this Mobile is in gets activated. /// public virtual void OnSectorActivate() { } /// /// Overridable. Virtual event invoked when the sector this Mobile is in gets /// deactivated. /// 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 readonly 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(PromptStateCallback callback, PromptStateCallback cancelCallback, T state) => Prompt = new SimpleStatePrompt(callback, cancelCallback, state); public Prompt BeginPrompt(PromptStateCallback callback, bool callbackHandlesCancel, T state) => Prompt = new SimpleStatePrompt(callback, callbackHandlesCancel, state); public Prompt BeginPrompt(PromptStateCallback 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; } 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 SayTo(Mobile to, int number, int hue, string args = "") => to.NetState.SendMessageLocalized(Serial, Body, MessageType.Regular, hue, 3, number, Name, 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; } /// /// Overridable. Event invoked when the Mobile does a beneficial action. /// 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; } if (!ServerFeatureFlags.PvPCombat && Player && target.Player) { if (message) { SendLocalizedMessage(1001018); // You can not perform negative acts on your target. } return false; } // 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; /// /// Overridable. Event invoked when the Mobile does a harmful action. /// 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 && !ChangingCombatant) { 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(); _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; } /// /// Computes the total modified offset for the specified stat type. /// 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; } /// /// Overridable. Virtual event invoked when the changes. /// /// /// public virtual void OnRawStrChange(int oldValue) { } /// /// Overridable. Virtual event invoked when changes. /// /// /// public virtual void OnRawDexChange(int oldValue) { } /// /// Overridable. Virtual event invoked when the changes. /// /// /// public virtual void OnRawIntChange(int oldValue) { } /// /// Overridable. Virtual event invoked when the , , or /// changes. /// /// /// /// 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) { } /// /// Overridable. Event invoked when a call to failed because /// returned false: the Mobile was resistant to the poison. By default, this broadcasts an overhead message: * The poison /// seems to have no effect. * /// /// /// /// public virtual void OnPoisonImmunity(Mobile from, Poison poison) { PublicOverheadMessage(MessageType.Emote, 0x3B2, 1005534); // * The poison seems to have no effect. * } /// /// Overridable. Virtual event invoked when a call to failed because /// returned false: the Mobile was already poisoned by an equal or greater strength poison. /// /// /// /// public virtual void OnHigherPoison(Mobile from, Poison poison) { } /// /// Overridable. Event invoked when a call to succeeded. By default, this broadcasts an overhead /// message varying by the level of the poison. Example: * Zippy begins to spasm uncontrollably. * /// /// /// 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); } } /// /// Overridable. Called from , this method checks if the Mobile is immune to some /// . If true, will be invoked and /// is returned. /// /// /// /// public virtual bool CheckPoisonImmunity(Mobile from, Poison poison) => false; /// /// Overridable. Called from , this method checks if the Mobile is already poisoned by some /// of equal or greater strength. If true, will be invoked and /// is returned. /// /// /// /// public virtual bool CheckHigherPoison(Mobile from, Poison poison) => m_Poison != null && m_Poison.Level >= poison.Level; /// /// Overridable. Attempts to apply poison to the Mobile. Checks are made such that no /// higher poison is active and that the Mobile is not /// immune to the poison. Provided those assertions are true, the /// is applied and is invoked. /// /// /// /// /// One of four possible values: /// /// /// /// Cured /// /// The parameter was null and so was invoked. /// /// /// /// HigherPoisonActive /// /// The call to returned false. /// /// /// /// Immune /// /// The call to returned false. /// /// /// /// Poisoned /// /// The was successfully applied. /// /// /// 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; } /// /// Overridable. Called from , this method checks to see that the Mobile can be cured of /// /// /// /// public virtual bool CheckCure(Mobile from) => true; /// /// Overridable. Virtual event invoked when a call to succeeded. /// /// /// /// public virtual void OnCured(Mobile from, Poison oldPoison) { } /// /// Overridable. Virtual event invoked when a call to failed. /// /// /// /// public virtual void OnFailedCure(Mobile from) { } /// /// Overridable. Attempts to cure any poison that is currently active. /// /// True if poison was cured, false if otherwise. 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); /// /// Overridable. Event invoked when the Mobile is double clicked. By default, this method can either dismount or open the /// paperdoll. /// /// /// 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); } } /// /// Overridable. Virtual event invoked when the Mobile is double clicked by someone who is over 18 tiles away. /// /// public virtual void OnDoubleClickOutOfRange(Mobile from) { } /// /// 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. /// /// public virtual void OnDoubleClickCantSee(Mobile from) { } /// /// Overridable. Event invoked when the Mobile is double clicked by someone who is not alive. Similar to /// , this method will show the paperdoll. It does not, however, provide any dismount /// functionality. /// /// 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; } }