Reduces the world-save freeze window from ~740ms to ~78ms (measured on a synthetic 10M-entity / 1.7GB world, 24 cores, through the real pipeline classes) by removing the per-entity handoff between the game loop and the serialization workers, fixing how large indivisible payloads are scheduled, rewriting the BufferWriter hot path, removing per-entity placement state entirely, and finally replacing the global serialized-types tracking with a per-file type table (idx v4) that also shrinks idx files by ~21% and speeds the background write phase. The pipeline has also been validated end-to-end on live-copy worlds in the multi-million-entity range, where the freeze is drain-bound (real `Serialize()` costs far more CPU per byte than synthetic writes) — the same structural wins hold, and entity/file round-trips are byte-clean across both load paths.
## The problem
The freeze window is `max(main-thread handoff, slowest worker drain)`:
1. **The producer was the bottleneck.** The main thread round-robined every entity through per-worker `ConcurrentQueue`s — two interlocked ops per entity, ~740ms of freeze floor at 10M entities before any serialization happened.
2. **Round-robin distributes count, not cost.** "Deep" systems (50MB generic persistence blobs) and "thick" entities (100K-item storage keys) landed on arbitrary workers, producing lopsided drain times on large worlds.
3. **Worst-case scheduling.** `GenericEntityPersistence.Serialize` pushed its self-payload *after* all entities, and generic persistences sort last in the registry — so the biggest indivisible blobs started serializing at the very end, extending the freeze by their entire duration.
## The fix
**Commit 1 — chunked handoff + LPT scheduling + heap pre-sizing:**
- Pooled 4096-entity chunks published to one shared queue; workers pull chunks and load-balance dynamically (a worker busy with a thick entity simply takes fewer chunks).
- `Persistence.SerializeAll` pushes systems largest-first (LPT) using the previous save's payload size (or loaded file size on first boot); self-payloads get dedicated single-entity chunks so they overlap the entity stream instead of ending it.
- Worker heaps pre-size from the loaded save's `.bin` totals, eliminating copy-on-grow inside the first save's freeze.
- `SpinWait` backoff in the drain loop (never `Sleep(1)`), per-worker balance stats logged in debug builds (the call site is compiled out of Release), 1MB snapshot write buffer.
**Commit 2 — workers iterate the dictionaries directly + main thread joins the drain:**
- `GenericEntityPersistence` publishes 4096-slot ranges over its dictionary's backing entries array; workers serialize occupied slots (`value != null`) directly through a `ShadowEntry<TValue>` struct mirroring the runtime's private `Entry` layout. Safe because the dictionary is frozen during `Saving` (mutations divert to the pending safety queues).
- The layout is **proven at startup before any code reads through it**: validation measures the true `Entry` stride via precise allocation accounting (guaranteeing all shadow reads are in-bounds), then verifies every key/value of a churned, resized, freelist-exercised dictionary — reading value slots as raw pointer bits only, never materializing a managed reference until the layout is proven. If a future runtime changes `Dictionary` internals, validation fails with a logged warning and saves fall back to the (fully maintained) enumerate-and-push path.
- The main thread joins the drain via an inline worker after publishing, instead of idling — worth a full worker share, proportionally more on low-core hosts.
**Commit 3 — 2.2x faster BufferWriter write path, single-pass short strings:**
- PGO already devirtualizes and inlines every `IGenericWriter.Write` callsite (interface vs concrete measured identical) — the real per-write cost was the non-inlinable `Index` setter (range-check throw path + per-write high-water tracking) plus span bounds checks. Writes now reserve capacity once, then do an unaligned store through a ref with a raw index increment; the high-water mark folds at Seek/Resize instead of per write.
- Class-level implementations of the hottest default interface methods keep nested writes inlined (a DIM re-dispatches on `this` even at a devirtualized callsite).
- Strings of 85 chars or fewer encode once into a stack scratch instead of walking the string twice (`GetByteCount` + `GetBytes`). Byte output is identical.
- Measured: 34.4 → 15.7 ns/entity on a generated-style write mix; end-to-end freeze ~99ms → ~74-82ms.
**Commit 4 — branch-free fallback push loop:**
- A bare `foreach { PushToCache(entity); }` runs at 2.3ns/entity; the same loop carrying a per-entity heavy-check runs 2.3x slower — the cost is the fatter loop body defeating tight-loop codegen. Entity-level >1MB payloads are rare enough to ride in shared chunks; system self-payloads (the large ones) are still explicitly scheduled largest-first.
**Commit 5 — drop the 9-byte per-entity placement state; snapshots write from worker segment logs:**
- Every `ISerializable` carried `SerializedThread/SerializedPosition/SerializedLength` so `WriteSnapshot` could gather each entity's bytes from the worker heaps in dictionary order. But the idx records absolute positions — bin order is free — so the snapshot is now written in worker-heap order and the join inverts: workers log segments (owner, slot range, heap start) plus one length per record as they serialize; positions are implicit because a worker's writes are contiguous, and identity comes from re-walking the same snapshot slots in the same order (stable until `PostWorldSave`).
- Chunks are persistence-homogeneous (the partial chunk publishes at each `SerializeAll` boundary) so segments route to files by owner with zero per-entity state. Self-payloads keep placement as three private fields on the handful of persistence instances.
- Net: 9 bytes (plus padding) of resident state removed from every item, mobile, guild, and account on every shard; three interface-property stores per entity leave the drain hot path (stamping dirtied one cache line per entity mid-freeze — the lengths log is one sequential stream); each segment's bytes hit the bin as a single span write instead of one copy per entity, speeding the background write phase; and `IGenericSerializable` shrinks to just `Serialize(IGenericWriter)`. Transient cost: ~4 bytes per entity in pooled per-worker logs, released after each write. The save format was unchanged at this point (idx v3, same loader); the v4 bump comes later in the branch.
**Commit 6 — staged file writes replace memory-mapped snapshot writing:**
- `MemoryMapFileWriter` is removed. `FileBufferWriter` composes through the full `BufferWriter` raw write path into a pooled staging block that drains to the file as large sequential positional writes (`RandomAccess.Write`); seeks flush the block and move the file offset, so backwards patches (the idx entity count) become small positional writes.
- Memory-mapped composition paid a soft page fault on every composed page plus unpredictable dirty-section teardown stalls at dispose — measured ~4x slower end-to-end than staged writes at snapshot sizes.
**Commits 7–11 — idx v4: per-file type table replaces SerializedTypes.db and all runtime type tracking:**
- Previously every `Write(Type)` from every worker enqueued into a shared `ConcurrentQueue<Type>` during the freeze (interlocked writes on a shared cache line, millions of mostly-duplicate entries), the background phase drained and deduped it all into a `HashSet`, and the snapshot recomputed `xxHash64(Type.FullName)` once per entity record (~5.4M redundant hashes per save on a large world) to write 9-byte tag+hash idx records plus a global `SerializedTypes.db`.
- The db's only real job was diagnostics: the string name behind "Type `<X>` was not found. Delete all of those types?" during idx loading. That map now lives in the idx itself: each `GenericEntityPersistence<T>` keeps an insertion-ordered `Type -> ushort` table, hydrated at `AddEntity` and on every deserialize path — one dictionary `TryAdd` per entity add on the game thread, amortized across gameplay, and provably immutable while the background writer reads it (adds divert to the pending queues during saves).
- idx v4 layout: the table (names only) is written before the records; records reference it by 2-byte index, shrinking from 33 to 26 bytes (−21%). The loader resolves each table name **once** (`FindTypeByHash(ComputeHash64(name))` — semantically identical to v3 resolution, `TypeAlias` included) into a constructor array, and each record becomes an array index instead of an 8-byte hash read plus dictionary probe. The unresolved-type prompt now surfaces once per type, with the name.
- Deleted outright: `World.SerializedTypes`, the drain/dedupe pass in `WriteFiles`, `BufferWriter`'s type tracking (its `Write(Type)` is now pure — payload format unchanged: tag byte + xxHash64), `FileBufferWriter`'s typeSet parameter, `Persistence.WriteSerializedTypesSnapshot`, and the adhoc db write. SerializedTypes.db is no longer produced.
- **Backward compatibility:** v0–v3 saves (including their SerializedTypes.db and legacy tdb files) load exactly as before, and every legacy load path hydrates the new table so the first v4 save after an upgrade is complete. Stale db files in existing save folders are simply ignored. Verified live: a v3 save boots, saves as v4 (Items.idx −20.2% on a dev world), and reloads with identical entity counts.
## Measured (synthetic 10M entities / 1.7GB, 64+64+32MB system blobs, 24x 2MB thick entities, dense write profile, 24 cores)
| Metric | Before | After |
|---|---|---|
| Steady-state freeze | ~740 ms | **~78 ms** |
| Main-thread publish cost | ~740 ms | **~0.1 ms** |
| Steady-state allocations | 0 | 0 (by iter 2) |
| Worker byte-load spread | 2x | ~1.15x |
The freeze is now bound by pure serialize throughput (payload / cores).
## Tests
- 779 Server.Tests + 501 UOContent.Tests pass.
- New across the branch: chunk fill/flush/owner-boundary tests, pool reuse/clear tests, an end-to-end multi-worker drain through the real wake/push/flush/pause protocol, a 50K-entry churn equivalence test for the shadow iteration re-walk (the exact pairing the snapshot writer relies on), byte-level BufferWriter output/position pins, `RuntimeLayoutIsSupported` so a silent fallback on a future runtime upgrade fails loudly in CI, and a full snapshot **round-trip test** that serializes 25K entities plus a self-payload through real workers, writes the idx/bin from the segment logs, and reloads them through the standard loader (now in v4 format).
- For idx v4 specifically: `FileBufferWriter` staging/drain/seek-patch and oversized-item tests, type-table registration tests, a hand-written v4 fixture proving an unresolvable type name skips only its own records through the console confirmation flow, and a hand-written **legacy v3 fixture** proving old saves still load and hydrate the type table for their next save.
## Trade-offs
- Chunk scheduling is nondeterministic, so worker heaps ratchet to each worker's max-ever draw rather than a fixed share. With slot ranges the balance is tight (~1.15x), so the effect is small; a shared slab pool remains an option if production shows retention creep.
- Entity-level heavy items inside slot ranges are serialized wherever they're encountered (no LPT for them); worst-case tail is one thick entity's serialize time (~ms). System self-payloads — the large ones — are still explicitly scheduled largest-first.
- Snapshot-write error granularity is per segment rather than per entity (heap-bounds bugs were the only thing the per-entity catch ever caught; idx metadata reads keep per-record granularity).
- idx v4 is a save-format version bump: old saves load unchanged through the preserved legacy paths, but saves written by this branch require this loader. Per-persistence type tables cap at 65,535 distinct entity types per boot (hard throw, orders of magnitude of headroom), and a type's table slot persists until restart even if its last entity is deleted — a few stale name entries per file, by design.
8995 lines
236 KiB
C#
8995 lines
236 KiB
C#
/*************************************************************************
|
|
* 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 <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using Server.Accounting;
|
|
using Server.Collections;
|
|
using Server.ContextMenus;
|
|
using Server.Guilds;
|
|
using Server.HuePickers;
|
|
using Server.Items;
|
|
using Server.Logging;
|
|
using Server.Menus;
|
|
using Server.Mobiles;
|
|
using Server.Network;
|
|
using Server.Prompts;
|
|
using Server.Targeting;
|
|
using System;
|
|
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<in T>(Mobile from, object targeted, T state);
|
|
|
|
public delegate void PromptCallback(Mobile from, string text);
|
|
|
|
public delegate void PromptStateCallback<in T>(Mobile from, string text, T state);
|
|
|
|
public class DamageEntry
|
|
{
|
|
public DamageEntry(Mobile damager) => Damager = damager;
|
|
|
|
public Mobile Damager { get; }
|
|
|
|
public int DamageGiven { get; set; }
|
|
|
|
public DateTime LastDamage { get; set; }
|
|
|
|
public bool HasExpired => Core.Now > LastDamage + ExpireDelay;
|
|
|
|
public List<DamageEntry> Responsible { get; set; }
|
|
|
|
public static TimeSpan ExpireDelay { get; set; } = TimeSpan.FromMinutes(2.0);
|
|
}
|
|
|
|
[Flags]
|
|
public enum StatType : 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<Item> initialContent, List<Item> equippedItems
|
|
);
|
|
|
|
public delegate int AOSStatusHandler(Mobile from, int index);
|
|
|
|
/// <summary>
|
|
/// Base class representing players, npcs, and creatures.
|
|
/// </summary>
|
|
public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPropertyListEntity, IValueLinkListNode<Mobile>
|
|
{
|
|
// Allow four warmode changes in 0.5 seconds, any more will be delay for two seconds
|
|
private const int WarmodeCatchCount = 4;
|
|
|
|
private static readonly ILogger logger = LogFactory.GetLogger(typeof(Mobile));
|
|
|
|
// TODO: Make these configurations
|
|
private static readonly TimeSpan WarmodeSpamCatch = TimeSpan.FromSeconds(Core.SE ? 1.0 : 0.5);
|
|
private static readonly TimeSpan WarmodeSpamDelay = TimeSpan.FromSeconds(Core.SE ? 4.0 : 2.0);
|
|
private static readonly TimeSpan ExpireCombatantDelay = TimeSpan.FromMinutes(1.0);
|
|
private static readonly TimeSpan ExpireAggressorsDelay = TimeSpan.FromSeconds(5.0);
|
|
|
|
private static readonly object m_GhostMutateContext = new();
|
|
|
|
private static readonly List<Mobile> m_Hears = new();
|
|
private static readonly List<IEntity> m_OnSpeech = new();
|
|
|
|
private static readonly string[] m_AccessLevelNames =
|
|
{
|
|
"a player",
|
|
"a counselor",
|
|
"a game master",
|
|
"a seer",
|
|
"an administrator",
|
|
"a developer",
|
|
"an owner"
|
|
};
|
|
|
|
private static readonly int[] m_InvalidBodies =
|
|
{
|
|
32,
|
|
95,
|
|
156,
|
|
197,
|
|
198
|
|
};
|
|
|
|
private static readonly Queue<Mobile> m_DeltaQueue = new();
|
|
|
|
private static readonly string[] m_GuildTypes =
|
|
{
|
|
"",
|
|
" (Chaos)",
|
|
" (Order)"
|
|
};
|
|
|
|
private static bool _disableCastParalyze = true;
|
|
|
|
public static void Configure()
|
|
{
|
|
_disableCastParalyze = ServerConfiguration.GetSetting("spellCasting.disableCastParalyze", true);
|
|
}
|
|
|
|
private List<object> _actions;
|
|
private AccessLevel m_AccessLevel;
|
|
|
|
private TimerExecutionToken _autoManifestTimerToken;
|
|
|
|
private Container m_Backpack;
|
|
|
|
private BankBox m_BankBox;
|
|
private Body m_Body;
|
|
private Body m_BodyMod;
|
|
private bool m_CanHearGhosts;
|
|
|
|
private int m_ChangingCombatant;
|
|
private Mobile m_Combatant;
|
|
private TimerExecutionToken _combatTimerToken;
|
|
private bool m_Criminal;
|
|
|
|
private MobileDelta m_DeltaFlags;
|
|
private Direction m_Direction;
|
|
private bool m_DisplayGuildTitle;
|
|
|
|
private TimerExecutionToken _expireAggrTimerToken;
|
|
private TimerExecutionToken _expireCombatantTimerToken;
|
|
private TimerExecutionToken _expireCriminalTimerToken;
|
|
private 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<StatMod> _statMods;
|
|
private List<ResistanceMod> _resistanceMods;
|
|
private List<SkillMod> _skillMods;
|
|
|
|
public Mobile()
|
|
{
|
|
m_Region = Map.Internal.DefaultRegion;
|
|
Serial = World.NewMobile;
|
|
|
|
DefaultMobileInit();
|
|
|
|
World.AddEntity(this);
|
|
}
|
|
|
|
public Mobile(Serial serial)
|
|
{
|
|
m_Region = Map.Internal.DefaultRegion;
|
|
Serial = serial;
|
|
Aggressors = new List<AggressorInfo>();
|
|
Aggressed = new List<AggressorInfo>();
|
|
NextSkillTime = Core.TickCount;
|
|
DamageEntries = new List<DamageEntry>();
|
|
}
|
|
|
|
// Sectors
|
|
public Mobile Next { get; set; }
|
|
public Mobile Previous { get; set; }
|
|
public bool OnLinkList { get; set; }
|
|
|
|
public static bool DragEffects { get; set; } = true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Race Race
|
|
{
|
|
get => m_Race ??= Race.DefaultRace;
|
|
set
|
|
{
|
|
var oldRace = Race;
|
|
|
|
m_Race = value ?? Race.DefaultRace;
|
|
|
|
Body = m_Race.Body(this);
|
|
UpdateResistances();
|
|
|
|
Delta(MobileDelta.Race);
|
|
|
|
OnRaceChange(oldRace);
|
|
}
|
|
}
|
|
|
|
public virtual double RacialSkillBonus => 0;
|
|
|
|
public int[] Resistances { get; private set; }
|
|
|
|
public virtual int BasePhysicalResistance => 0;
|
|
public virtual int BaseFireResistance => 0;
|
|
public virtual int BaseColdResistance => 0;
|
|
public virtual int BasePoisonResistance => 0;
|
|
public virtual int BaseEnergyResistance => 0;
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int PhysicalResistance => GetResistance(ResistanceType.Physical);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int FireResistance => GetResistance(ResistanceType.Fire);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int ColdResistance => GetResistance(ResistanceType.Cold);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int PoisonResistance => GetResistance(ResistanceType.Poison);
|
|
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual int EnergyResistance => GetResistance(ResistanceType.Energy);
|
|
|
|
public static int MaxPlayerResistance { get; set; } = 70;
|
|
|
|
public virtual bool NewGuildDisplay => false;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual bool IgnoreMobiles
|
|
{
|
|
get => _ignoreMobiles;
|
|
set
|
|
{
|
|
if (_ignoreMobiles != value)
|
|
{
|
|
_ignoreMobiles = value;
|
|
Delta(MobileDelta.Flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
public object Party { get; set; }
|
|
|
|
public List<SkillMod> SkillMods => _skillMods;
|
|
public List<StatMod> StatMods => _statMods;
|
|
public List<ResistanceMod> ResistanceMods => _resistanceMods;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int VirtualArmorMod
|
|
{
|
|
get => m_VirtualArmorMod;
|
|
set
|
|
{
|
|
if (m_VirtualArmorMod != value)
|
|
{
|
|
m_VirtualArmorMod = value;
|
|
|
|
Delta(MobileDelta.Armor);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int MeleeDamageAbsorb { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int MagicDamageAbsorb { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SkillsTotal => Skills?.Total ?? 0;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int SkillsCap
|
|
{
|
|
get => Skills?.Cap ?? 0;
|
|
set
|
|
{
|
|
if (Skills != null)
|
|
{
|
|
Skills.Cap = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int BaseSoundID { get; set; }
|
|
|
|
public long NextCombatTime { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int NameHue { get; set; } = -1;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Hunger
|
|
{
|
|
get => m_Hunger;
|
|
set
|
|
{
|
|
var oldValue = m_Hunger;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_Hunger = value;
|
|
OnHungerChanged(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Thirst
|
|
{
|
|
get => _thirst;
|
|
set
|
|
{
|
|
var oldValue = _thirst;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
_thirst = value;
|
|
OnThirstChanged(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int BAC { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the number of steps this player may take when hidden before being revealed.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int AllowedStealthSteps { get; set; }
|
|
|
|
public Item Holding
|
|
{
|
|
get => m_Holding;
|
|
set
|
|
{
|
|
if (m_Holding != value)
|
|
{
|
|
if (m_Holding != null)
|
|
{
|
|
UpdateTotal(m_Holding, TotalType.Weight, -(m_Holding.TotalWeight + m_Holding.PileWeight));
|
|
|
|
if (m_Holding.HeldBy == this)
|
|
{
|
|
m_Holding.HeldBy = null;
|
|
}
|
|
}
|
|
|
|
if (value != null && m_Holding != null)
|
|
{
|
|
DropHolding();
|
|
}
|
|
|
|
m_Holding = value;
|
|
|
|
if (m_Holding != null)
|
|
{
|
|
UpdateTotal(m_Holding, TotalType.Weight, m_Holding.TotalWeight + m_Holding.PileWeight);
|
|
|
|
m_Holding.HeldBy ??= this;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public long LastMoveTime { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual bool Paralyzed
|
|
{
|
|
get => m_Paralyzed;
|
|
set
|
|
{
|
|
if (m_Paralyzed != value)
|
|
{
|
|
m_Paralyzed = value;
|
|
Delta(MobileDelta.Flags);
|
|
|
|
SendLocalizedMessage(m_Paralyzed ? 502381 : 502382);
|
|
_paraTimerToken.Cancel();
|
|
}
|
|
|
|
if (!value && m_NetState != null)
|
|
{
|
|
var now = Core.TickCount;
|
|
if (now - m_NetState._nextMovementTime > 0)
|
|
{
|
|
m_NetState._nextMovementTime = now;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool DisarmReady { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool StunReady { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Frozen
|
|
{
|
|
get => m_Frozen;
|
|
set
|
|
{
|
|
if (m_Frozen != value)
|
|
{
|
|
m_Frozen = value;
|
|
Delta(MobileDelta.Flags);
|
|
_frozenTimerToken.Cancel();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawStr" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType StrLock
|
|
{
|
|
get => m_StrLock;
|
|
set
|
|
{
|
|
if (m_StrLock != value)
|
|
{
|
|
m_StrLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawDex" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType DexLock
|
|
{
|
|
get => m_DexLock;
|
|
set
|
|
{
|
|
if (m_DexLock != value)
|
|
{
|
|
m_DexLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the <see cref="StatLockType">lock state</see> for the <see cref="RawInt" /> property.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public StatLockType IntLock
|
|
{
|
|
get => m_IntLock;
|
|
set
|
|
{
|
|
if (m_IntLock != value)
|
|
{
|
|
m_IntLock = value;
|
|
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
}
|
|
}
|
|
|
|
[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<AggressorInfo> Aggressors { get; private set; }
|
|
|
|
public List<AggressorInfo> Aggressed { get; private set; }
|
|
|
|
public bool ChangingCombatant => m_ChangingCombatant > 0;
|
|
|
|
private void ExpireCombatant()
|
|
{
|
|
Combatant = null;
|
|
_expireCombatantTimerToken.Cancel();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets or sets which Mobile that this Mobile is currently engaged in combat with.
|
|
/// <seealso cref="OnCombatantChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual Mobile Combatant
|
|
{
|
|
get => m_Combatant;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Combatant != value && value != this)
|
|
{
|
|
var old = m_Combatant;
|
|
|
|
++m_ChangingCombatant;
|
|
m_Combatant = value;
|
|
|
|
if (m_Combatant != null && !CanBeHarmful(m_Combatant, false) ||
|
|
!Region.OnCombatantChange(this, old, m_Combatant))
|
|
{
|
|
m_Combatant = old;
|
|
--m_ChangingCombatant;
|
|
return;
|
|
}
|
|
|
|
if (m_Combatant == null)
|
|
{
|
|
m_NetState.SendChangeCombatant(Serial.Zero);
|
|
_expireCombatantTimerToken.Cancel();
|
|
_combatTimerToken.Cancel();
|
|
}
|
|
else
|
|
{
|
|
m_NetState.SendChangeCombatant(m_Combatant.Serial);
|
|
if (!_expireCombatantTimerToken.Running)
|
|
{
|
|
Timer.StartTimer(ExpireCombatantDelay, ExpireCombatant, out _expireCombatantTimerToken);
|
|
}
|
|
|
|
if (!_combatTimerToken.Running)
|
|
{
|
|
Timer.StartTimer(TimeSpan.FromSeconds(0.01), 0, CheckCombatTime, out _combatTimerToken);
|
|
}
|
|
|
|
if (CanBeHarmful(m_Combatant, false))
|
|
{
|
|
DoHarmful(m_Combatant); // due to reflection, might make m_Combatant null
|
|
m_Combatant?.PlaySound(m_Combatant.GetAngerSound());
|
|
}
|
|
}
|
|
|
|
OnCombatantChange();
|
|
--m_ChangingCombatant;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void CheckCombatTime()
|
|
{
|
|
if (Core.TickCount - NextCombatTime < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var combatant = Combatant;
|
|
|
|
// If no combatant, wrong map, one of us is a ghost, or cannot see, or deleted, then stop combat
|
|
if (combatant?.Deleted != false || Deleted || combatant.m_Map != m_Map ||
|
|
!combatant.Alive || !Alive || !CanSee(combatant) || combatant.IsDeadBondedPet ||
|
|
IsDeadBondedPet)
|
|
{
|
|
Combatant = null;
|
|
return;
|
|
}
|
|
|
|
var weapon = Weapon;
|
|
|
|
if (weapon == null || !InRange(combatant, weapon.MaxRange))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (InLOS(combatant))
|
|
{
|
|
weapon.OnBeforeSwing(this, combatant);
|
|
RevealingAction();
|
|
NextCombatTime =
|
|
Core.TickCount + (int)weapon.OnSwing(this, combatant).TotalMilliseconds;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalGold => GetTotal(TotalType.Gold);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalItems => GetTotal(TotalType.Items);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TotalWeight => GetTotal(TotalType.Weight);
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int TithingPoints
|
|
{
|
|
get => m_TithingPoints;
|
|
set
|
|
{
|
|
if (m_TithingPoints != value)
|
|
{
|
|
m_TithingPoints = value;
|
|
|
|
Delta(MobileDelta.TithingPoints);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Followers
|
|
{
|
|
get => m_Followers;
|
|
set
|
|
{
|
|
if (m_Followers != value)
|
|
{
|
|
m_Followers = value;
|
|
|
|
Delta(MobileDelta.Followers);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int FollowersMax
|
|
{
|
|
get => m_FollowersMax;
|
|
set
|
|
{
|
|
if (m_FollowersMax != value)
|
|
{
|
|
m_FollowersMax = value;
|
|
|
|
Delta(MobileDelta.Followers);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool TargetLocked { get; set; }
|
|
|
|
public Target Target
|
|
{
|
|
get => m_Target;
|
|
set
|
|
{
|
|
var oldTarget = m_Target;
|
|
var newTarget = value;
|
|
|
|
if (oldTarget == newTarget)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Target = null;
|
|
|
|
if (oldTarget != null && newTarget != null)
|
|
{
|
|
oldTarget.Cancel(this, TargetCancelType.Overridden);
|
|
}
|
|
|
|
m_Target = newTarget;
|
|
|
|
if (newTarget != null && !TargetLocked)
|
|
{
|
|
newTarget.SendTargetTo(m_NetState);
|
|
}
|
|
|
|
OnTargetChange();
|
|
}
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
public bool Pushing { get; set; }
|
|
|
|
public virtual bool IsDeadBondedPet => false;
|
|
|
|
[IgnoreDupe]
|
|
public ISpell Spell
|
|
{
|
|
get => m_Spell;
|
|
set
|
|
{
|
|
if (m_Spell != null && value != null)
|
|
{
|
|
logger.Warning("Spell has been overwritten.");
|
|
}
|
|
|
|
m_Spell = value;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Administrator)]
|
|
public bool AutoPageNotify { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.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;
|
|
|
|
/// <summary>
|
|
/// Overridable. Returns true if the player is alive, false if otherwise. By default, this is computed by:
|
|
/// <c>!Deleted && (!Player || !Body.IsGhost)</c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public virtual bool Alive => !Deleted && (!m_Player || !m_Body.IsGhost);
|
|
|
|
public static CreateCorpseHandler CreateCorpseHandler { get; set; }
|
|
|
|
public virtual bool RetainPackLocsOnDeath => Core.AOS;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Container Corpse { get; set; }
|
|
|
|
public static char[] GhostChars { get; set; }
|
|
public static bool NoSpeechLOS { get; set; }
|
|
|
|
public static TimeSpan AutoManifestTimeout { get; set; } = TimeSpan.FromSeconds(5.0);
|
|
|
|
public static bool InsuranceEnabled { get; set; }
|
|
|
|
public static int ActionDelay { get; set; } = 500;
|
|
|
|
public static VisibleDamageType VisibleDamageType { get; set; }
|
|
|
|
public List<DamageEntry> DamageEntries { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Mobile LastKiller { get; set; }
|
|
|
|
public static bool DefaultShowVisibleDamage { get; set; }
|
|
|
|
public static bool DefaultCanSeeVisibleDamage { get; set; }
|
|
|
|
public virtual bool ShowVisibleDamage => DefaultShowVisibleDamage;
|
|
public virtual bool CanSeeVisibleDamage => DefaultCanSeeVisibleDamage;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Squelched { get; set; }
|
|
|
|
public virtual bool ShouldCheckStatTimers => true;
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int LightLevel
|
|
{
|
|
get => m_LightLevel;
|
|
set
|
|
{
|
|
if (m_LightLevel != value)
|
|
{
|
|
m_LightLevel = value;
|
|
|
|
CheckLightLevels(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public string Profile { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public bool ProfileLocked { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Administrator)]
|
|
public bool Player
|
|
{
|
|
get => m_Player;
|
|
set
|
|
{
|
|
m_Player = value;
|
|
InvalidateProperties();
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public string Title
|
|
{
|
|
get => m_Title;
|
|
set
|
|
{
|
|
m_Title = value;
|
|
InvalidateProperties();
|
|
}
|
|
}
|
|
|
|
public List<Item> Items { get; private set; }
|
|
|
|
public virtual int MaxWeight => int.MaxValue;
|
|
|
|
public static IWeapon DefaultWeapon { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, canModify: true)]
|
|
public Skills Skills { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.Administrator)]
|
|
public AccessLevel AccessLevel
|
|
{
|
|
get => m_AccessLevel;
|
|
set
|
|
{
|
|
var oldValue = m_AccessLevel;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_AccessLevel = value;
|
|
Delta(MobileDelta.Noto);
|
|
InvalidateProperties();
|
|
|
|
SendMessage($"Your access level has been changed. You are now {GetAccessLevelName(value)}.");
|
|
|
|
ClearScreen();
|
|
SendEverything();
|
|
|
|
OnAccessLevelChanged(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Fame
|
|
{
|
|
get => m_Fame;
|
|
set
|
|
{
|
|
var oldValue = m_Fame;
|
|
|
|
if (oldValue != value)
|
|
{
|
|
m_Fame = value;
|
|
|
|
if (ShowFameTitle && (m_Player || m_Body.IsHuman) && oldValue >= 10000 != value >= 10000)
|
|
{
|
|
InvalidateProperties();
|
|
}
|
|
|
|
OnFameChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Karma
|
|
{
|
|
get => m_Karma;
|
|
set
|
|
{
|
|
var old = m_Karma;
|
|
|
|
if (old != value)
|
|
{
|
|
m_Karma = value;
|
|
OnKarmaChange(old);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Blessed
|
|
{
|
|
get => m_Blessed;
|
|
set
|
|
{
|
|
if (m_Blessed != value)
|
|
{
|
|
m_Blessed = value;
|
|
Delta(MobileDelta.HealthbarYellow);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int Luck => 0;
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int HueMod
|
|
{
|
|
get => m_HueMod;
|
|
set
|
|
{
|
|
if (m_HueMod != value)
|
|
{
|
|
m_HueMod = value;
|
|
|
|
Delta(MobileDelta.Hue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[Hue]
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Hue
|
|
{
|
|
get => m_HueMod != -1 ? m_HueMod : m_Hue;
|
|
set
|
|
{
|
|
var oldHue = m_Hue;
|
|
|
|
if (oldHue != value)
|
|
{
|
|
m_Hue = value;
|
|
|
|
Delta(MobileDelta.Hue);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Direction Direction
|
|
{
|
|
get => m_Direction;
|
|
set
|
|
{
|
|
if (m_Direction != value)
|
|
{
|
|
m_Direction = value;
|
|
Delta(MobileDelta.Direction);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Female
|
|
{
|
|
get => m_Female;
|
|
set
|
|
{
|
|
if (m_Female != value)
|
|
{
|
|
m_Female = value;
|
|
Delta(MobileDelta.Flags);
|
|
OnGenderChanged(!m_Female);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Flying
|
|
{
|
|
get => m_Flying;
|
|
set
|
|
{
|
|
if (m_Flying != value)
|
|
{
|
|
m_Flying = value;
|
|
Delta(MobileDelta.Flags);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Warmode
|
|
{
|
|
get => m_Warmode;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Warmode != value)
|
|
{
|
|
_autoManifestTimerToken.Cancel();
|
|
|
|
m_Warmode = value;
|
|
Delta(MobileDelta.Flags);
|
|
|
|
m_NetState.SendSetWarMode(value);
|
|
|
|
if (!m_Warmode)
|
|
{
|
|
Combatant = null;
|
|
}
|
|
|
|
if (!Alive)
|
|
{
|
|
if (value)
|
|
{
|
|
Delta(MobileDelta.GhostUpdate);
|
|
}
|
|
else
|
|
{
|
|
SendRemovePacket(false);
|
|
}
|
|
}
|
|
|
|
OnWarmodeChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Hidden
|
|
{
|
|
get => m_Hidden;
|
|
set
|
|
{
|
|
if (m_Hidden != value)
|
|
{
|
|
m_Hidden = value;
|
|
// Delta( MobileDelta.Flags );
|
|
|
|
OnHiddenChanged();
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster, AccessLevel.Owner)]
|
|
public NetState NetState
|
|
{
|
|
get
|
|
{
|
|
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<BankBox>(Layer.Bank);
|
|
|
|
if (m_BankBox == null)
|
|
{
|
|
AddItem(m_BankBox = new BankBox(this));
|
|
}
|
|
|
|
return m_BankBox;
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Container Backpack
|
|
{
|
|
get
|
|
{
|
|
if (m_Backpack?.Deleted != false || m_Backpack.Parent != this)
|
|
{
|
|
m_Backpack = FindItemOnLayer<Container>(Layer.Backpack);
|
|
}
|
|
|
|
return m_Backpack;
|
|
}
|
|
}
|
|
|
|
public virtual bool KeepsItemsOnDeath => m_AccessLevel > AccessLevel.Player;
|
|
|
|
public bool HasTrade => m_NetState?.Trades.Count > 0;
|
|
|
|
public bool NoMoveHS { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.Counselor, AccessLevel.GameMaster)]
|
|
public int Kills
|
|
{
|
|
get => m_Kills;
|
|
set
|
|
{
|
|
var oldValue = m_Kills;
|
|
|
|
if (m_Kills != value)
|
|
{
|
|
m_Kills = Math.Max(value, 0);
|
|
|
|
if (oldValue >= 5 != 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;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the maximum attainable value for <see cref="RawStr" />, <see cref="RawDex" />, and <see cref="RawInt" />.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int StatCap
|
|
{
|
|
get => m_StatCap;
|
|
set
|
|
{
|
|
if (m_StatCap != value)
|
|
{
|
|
m_StatCap = value;
|
|
|
|
Delta(MobileDelta.StatCap);
|
|
}
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool Meditating { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CanSwim { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CantWalk { get; set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public bool CanHearGhosts
|
|
{
|
|
get => m_CanHearGhosts || AccessLevel >= AccessLevel.Counselor;
|
|
set => m_CanHearGhosts = value;
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawStatTotal => RawStr + RawDex + RawInt;
|
|
|
|
public long NextSpellTime { get; set; }
|
|
|
|
public static AllowBeneficialHandler AllowBeneficialHandler { get; set; }
|
|
|
|
public static AllowHarmfulHandler AllowHarmfulHandler { get; set; }
|
|
|
|
public static SkillCheckTargetHandler SkillCheckTargetHandler { get; set; }
|
|
|
|
public static SkillCheckLocationHandler SkillCheckLocationHandler { get; set; }
|
|
|
|
public static SkillCheckDirectTargetHandler SkillCheckDirectTargetHandler { get; set; }
|
|
|
|
public static SkillCheckDirectLocationHandler SkillCheckDirectLocationHandler { get; set; }
|
|
|
|
public static AOSStatusHandler AOSStatusHandler { get; set; }
|
|
|
|
public static RegenRateHandler HitsRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultHitsRate { get; set; }
|
|
|
|
public static RegenRateHandler StamRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultStamRate { get; set; }
|
|
|
|
public static RegenRateHandler ManaRegenRateHandler { get; set; }
|
|
|
|
public static TimeSpan DefaultManaRate { get; set; }
|
|
|
|
public static TimeSpan ExpireCriminalDelay { get; set; } = TimeSpan.FromMinutes(2.0);
|
|
|
|
public Prompt Prompt
|
|
{
|
|
get => m_Prompt;
|
|
set
|
|
{
|
|
var oldPrompt = m_Prompt;
|
|
var newPrompt = value;
|
|
|
|
if (oldPrompt == newPrompt)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Prompt = null;
|
|
|
|
// TODO: Cancel the prompt anyway?
|
|
if (newPrompt != null)
|
|
{
|
|
oldPrompt?.OnCancel(this);
|
|
}
|
|
|
|
m_Prompt = newPrompt;
|
|
NetState.SendPrompt(newPrompt);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, strength of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Str" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawStrChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawStr
|
|
{
|
|
get => m_Str;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Str != value)
|
|
{
|
|
var oldValue = m_Str;
|
|
|
|
m_Str = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Hits);
|
|
|
|
if (Hits < HitsMax)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else if (Hits > HitsMax)
|
|
{
|
|
Hits = HitsMax;
|
|
}
|
|
|
|
OnRawStrChange(oldValue);
|
|
OnRawStatChange(StatType.Str, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective strength of the Mobile. This is the sum of the <see cref="RawStr" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawStr" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Str
|
|
{
|
|
get => Math.Clamp(m_Str + GetStatOffset(StatType.Str), 1, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawStr = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, dexterity of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Dex" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawDexChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawDex
|
|
{
|
|
get => m_Dex;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Dex != value)
|
|
{
|
|
var oldValue = m_Dex;
|
|
|
|
m_Dex = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Stam);
|
|
|
|
if (Stam < StamMax)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else if (Stam > StamMax)
|
|
{
|
|
Stam = StamMax;
|
|
}
|
|
|
|
OnRawDexChange(oldValue);
|
|
OnRawStatChange(StatType.Dex, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective dexterity of the Mobile. This is the sum of the <see cref="RawDex" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawDex" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Dex
|
|
{
|
|
get => Math.Clamp(m_Dex + GetStatOffset(StatType.Dex), 0, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawDex = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the base, unmodified, intelligence of the Mobile. Ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="Int" />
|
|
/// <seealso cref="StatMod" />
|
|
/// <seealso cref="OnRawIntChange" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int RawInt
|
|
{
|
|
get => m_Int;
|
|
set
|
|
{
|
|
value = Math.Clamp(value, 1, 65000);
|
|
|
|
if (m_Int != value)
|
|
{
|
|
var oldValue = m_Int;
|
|
|
|
m_Int = value;
|
|
Delta(MobileDelta.Stat | MobileDelta.Mana);
|
|
|
|
if (Mana < ManaMax)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else if (Mana > ManaMax)
|
|
{
|
|
Mana = ManaMax;
|
|
}
|
|
|
|
OnRawIntChange(oldValue);
|
|
OnRawStatChange(StatType.Int, oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the effective intelligence of the Mobile. This is the sum of the <see cref="RawInt" /> plus any additional
|
|
/// modifiers. Any attempts to set this value when under the influence of a <see cref="StatMod" /> will result in no change.
|
|
/// It ranges from 1 to 65000, inclusive.
|
|
/// <seealso cref="RawInt" />
|
|
/// <seealso cref="StatMod" />
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int Int
|
|
{
|
|
get => Math.Clamp(m_Int + GetStatOffset(StatType.Int), 0, 65000);
|
|
set
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
RawInt = value;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current hit point of the Mobile. This value ranges from 0 to <see cref="HitsMax" />, inclusive. When
|
|
/// set
|
|
/// to the value of <see cref="HitsMax" />, the <see cref="AggressorInfo.CanReportMurder">CanReportMurder</see> flag of all
|
|
/// aggressors is reset to false, and the list of damage entries is cleared.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Hits
|
|
{
|
|
get => m_Hits;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, HitsMax);
|
|
|
|
if (value == HitsMax)
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
|
|
for (var i = 0; i < Aggressors.Count; i++) // reset reports on full HP
|
|
{
|
|
Aggressors[i].CanReportMurder = false;
|
|
}
|
|
|
|
if (DamageEntries.Count > 0)
|
|
{
|
|
DamageEntries.Clear(); // reset damage entries on full HP
|
|
}
|
|
}
|
|
else if (CanRegenHits)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
}
|
|
|
|
if (m_Hits != value)
|
|
{
|
|
var oldValue = m_Hits;
|
|
m_Hits = value;
|
|
Delta(MobileDelta.Hits);
|
|
OnHitsChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum hit point of the Mobile. By default, this returns: <c>50 + (<see cref="Str" /> / 2)</c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int HitsMax => 50 + Str / 2;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="StamMax" />, inclusive.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Stam
|
|
{
|
|
get => m_Stam;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, StamMax);
|
|
|
|
if (CanRegenStam && value < StamMax)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_StamTimer?.Stop();
|
|
}
|
|
|
|
if (m_Stam != value)
|
|
{
|
|
var oldValue = m_Stam;
|
|
m_Stam = value;
|
|
Delta(MobileDelta.Stam);
|
|
OnStamChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum stamina of the Mobile. By default, this returns:
|
|
/// <c>
|
|
/// <see cref="Dex" />
|
|
/// </c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int StamMax => Dex;
|
|
|
|
/// <summary>
|
|
/// Gets or sets the current stamina of the Mobile. This value ranges from 0 to <see cref="ManaMax" />, inclusive.
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int Mana
|
|
{
|
|
get => m_Mana;
|
|
set
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
value = Math.Clamp(value, 0, ManaMax);
|
|
|
|
if (value == ManaMax)
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
|
|
if (Meditating)
|
|
{
|
|
Meditating = false;
|
|
SendLocalizedMessage(501846); // You are at peace.
|
|
}
|
|
}
|
|
else if (CanRegenMana)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
}
|
|
|
|
if (m_Mana != value)
|
|
{
|
|
var oldValue = m_Mana;
|
|
m_Mana = value;
|
|
Delta(MobileDelta.Mana);
|
|
OnManaChange(oldValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Gets the maximum mana of the Mobile. By default, this returns:
|
|
/// <c>
|
|
/// <see cref="Int" />
|
|
/// </c>
|
|
/// </summary>
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public virtual int ManaMax => Int;
|
|
|
|
public Timer PoisonTimer { get; private set; }
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public Poison Poison
|
|
{
|
|
get => m_Poison;
|
|
set
|
|
{
|
|
m_Poison = value;
|
|
Delta(MobileDelta.HealthbarPoison);
|
|
|
|
if (PoisonTimer != null)
|
|
{
|
|
PoisonTimer.Stop();
|
|
PoisonTimer = null;
|
|
}
|
|
|
|
if (m_Poison != null)
|
|
{
|
|
PoisonTimer = m_Poison.ConstructTimer(this);
|
|
|
|
PoisonTimer?.Start();
|
|
}
|
|
|
|
CheckStatTimers();
|
|
}
|
|
}
|
|
|
|
[CommandProperty(AccessLevel.GameMaster)]
|
|
public int HairItemID
|
|
{
|
|
get => _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 => m_PropertyList ??= InitializePropertyList(new ObjectPropertyList(this));
|
|
|
|
public virtual void GetProperties(IPropertyList list)
|
|
{
|
|
AddNameProperties(list);
|
|
}
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
|
public DateTime Created { get; set; } = Core.Now;
|
|
|
|
[IgnoreDupe]
|
|
[CommandProperty(AccessLevel.Counselor)]
|
|
public Serial Serial { get; }
|
|
|
|
public virtual void Serialize(IGenericWriter writer)
|
|
{
|
|
writer.Write(37); // version
|
|
|
|
writer.WriteDeltaTime(LastStrGain);
|
|
writer.WriteDeltaTime(LastIntGain);
|
|
writer.WriteDeltaTime(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<ResistanceMod>();
|
|
_resistanceMods.Add(mod);
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual ResistanceMod GetResistanceMod(string name)
|
|
{
|
|
if (_resistanceMods == null || name == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (var i = 0; i < _resistanceMods.Count; i++)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
return mod;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void RemoveResistanceMod(string name)
|
|
{
|
|
if (_resistanceMods == null || name == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _resistanceMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
_resistanceMods.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
if (_resistanceMods.Count == 0)
|
|
{
|
|
_resistanceMods = null;
|
|
}
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual void RemoveResistanceMod(ResistanceMod mod)
|
|
{
|
|
if (_resistanceMods == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_resistanceMods.Remove(mod);
|
|
|
|
if (_resistanceMods.Count == 0)
|
|
{
|
|
_resistanceMods = null;
|
|
}
|
|
|
|
UpdateResistances();
|
|
}
|
|
|
|
public virtual void ComputeResistances()
|
|
{
|
|
Resistances ??= new[] { int.MinValue, int.MinValue, int.MinValue, int.MinValue, int.MinValue };
|
|
|
|
for (var i = 0; i < Resistances.Length; ++i)
|
|
{
|
|
Resistances[i] = 0;
|
|
}
|
|
|
|
Resistances[0] += BasePhysicalResistance;
|
|
Resistances[1] += BaseFireResistance;
|
|
Resistances[2] += BaseColdResistance;
|
|
Resistances[3] += BasePoisonResistance;
|
|
Resistances[4] += BaseEnergyResistance;
|
|
|
|
if (_resistanceMods != null)
|
|
{
|
|
for (var i = 0; i < _resistanceMods.Count; i++)
|
|
{
|
|
var mod = _resistanceMods[i];
|
|
var v = (int)mod.Type;
|
|
|
|
if (v >= 0 && v < Resistances.Length)
|
|
{
|
|
Resistances[v] += mod.Offset;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
var item = Items[i];
|
|
|
|
if (item.CheckPropertyConflict(this))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Resistances[0] += item.PhysicalResistance;
|
|
Resistances[1] += item.FireResistance;
|
|
Resistances[2] += item.ColdResistance;
|
|
Resistances[3] += item.PoisonResistance;
|
|
Resistances[4] += item.EnergyResistance;
|
|
}
|
|
|
|
for (var i = 0; i < Resistances.Length; ++i)
|
|
{
|
|
var min = GetMinResistance((ResistanceType)i);
|
|
var max = GetMaxResistance((ResistanceType)i);
|
|
|
|
if (max < min)
|
|
{
|
|
max = min;
|
|
}
|
|
|
|
if (Resistances[i] > max)
|
|
{
|
|
Resistances[i] = max;
|
|
}
|
|
else if (Resistances[i] < min)
|
|
{
|
|
Resistances[i] = min;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual int GetMinResistance(ResistanceType type) => int.MinValue;
|
|
|
|
public virtual int GetMaxResistance(ResistanceType type) => m_Player ? MaxPlayerResistance : int.MaxValue;
|
|
|
|
public int GetAOSStatus(int index) => AOSStatusHandler?.Invoke(this, index) ?? 0;
|
|
|
|
public virtual void SendPropertiesTo(NetState ns)
|
|
{
|
|
ns?.Send(PropertyList.Buffer);
|
|
}
|
|
|
|
public virtual void OnAosSingleClick(Mobile from)
|
|
{
|
|
var opl = PropertyList;
|
|
|
|
if (opl.Header > 0)
|
|
{
|
|
int hue;
|
|
|
|
if (NameHue != -1)
|
|
{
|
|
hue = NameHue;
|
|
}
|
|
else if (m_AccessLevel > AccessLevel.Player)
|
|
{
|
|
hue = 11;
|
|
}
|
|
else
|
|
{
|
|
hue = Notoriety.GetHue(Notoriety.Compute(from, this));
|
|
}
|
|
|
|
from.NetState.SendMessageLocalized(Serial, Body, MessageType.Label, hue, 3, opl.Header, Name, opl.HeaderArgs);
|
|
}
|
|
}
|
|
|
|
public virtual string ApplyNameSuffix(string suffix) => suffix;
|
|
|
|
public virtual void AddNameProperties(IPropertyList list)
|
|
{
|
|
string prefix;
|
|
|
|
if (ShowFameTitle && (m_Player || m_Body.IsHuman) && m_Fame >= 10000)
|
|
{
|
|
prefix = m_Female ? "Lady " : "Lord ";
|
|
}
|
|
else
|
|
{
|
|
prefix = " ";
|
|
}
|
|
|
|
var guild = m_Guild;
|
|
var hasTitle = PropertyTitle && !string.IsNullOrEmpty(Title);
|
|
var hasGuild = guild != null && (m_Player || m_DisplayGuildTitle);
|
|
|
|
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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="skill" /> changes in some way.
|
|
/// </summary>
|
|
public virtual void OnSkillInvalidated(Skill skill)
|
|
{
|
|
}
|
|
|
|
public virtual void ValidateSkillMods()
|
|
{
|
|
if (_skillMods == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _skillMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _skillMods[i];
|
|
|
|
if (!mod.CheckCondition())
|
|
{
|
|
_skillMods.RemoveAt(i);
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void AddSkillMod(SkillMod mod)
|
|
{
|
|
if (mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
_skillMods ??= new List<SkillMod>();
|
|
_skillMods.Add(mod);
|
|
mod.Owner = this;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
|
|
public virtual SkillMod GetSkillMod(string name)
|
|
{
|
|
if (_skillMods == null || name == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
for (var i = 0; i < _skillMods.Count; i++)
|
|
{
|
|
var mod = _skillMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
return mod;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public virtual void RemoveSkillMod(string name)
|
|
{
|
|
if (_skillMods == null || name == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
for (var i = _skillMods.Count - 1; i >= 0; i--)
|
|
{
|
|
var mod = _skillMods[i];
|
|
if (mod.Name == name)
|
|
{
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
_skillMods.RemoveAt(i);
|
|
}
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
if (_skillMods.Count == 0)
|
|
{
|
|
_skillMods = null;
|
|
}
|
|
}
|
|
|
|
public virtual void RemoveSkillMod(SkillMod mod)
|
|
{
|
|
if (_skillMods == null || mod == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (_skillMods.Remove(mod))
|
|
{
|
|
mod.Owner = null;
|
|
Skills[mod.Skill]?.Update();
|
|
}
|
|
|
|
ValidateSkillMods();
|
|
|
|
if (_skillMods.Count == 0)
|
|
{
|
|
_skillMods = null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a client, <paramref name="from" />, invokes a 'help request' for the Mobile.
|
|
/// Seemingly no longer functional in newer clients.
|
|
/// </summary>
|
|
public virtual void OnHelpRequest(Mobile from)
|
|
{
|
|
}
|
|
|
|
public void DelayChangeWarmode(bool value)
|
|
{
|
|
if (m_WarmodeChanges > WarmodeCatchCount)
|
|
{
|
|
_warmodeSpamValue = value;
|
|
m_WarmodeChanges++;
|
|
return;
|
|
}
|
|
|
|
if (m_Warmode == value)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var now = Core.Now;
|
|
var next = m_NextWarmodeChange;
|
|
|
|
if (now > next || m_WarmodeChanges == 0)
|
|
{
|
|
m_WarmodeChanges = 1;
|
|
m_NextWarmodeChange = now + WarmodeSpamCatch;
|
|
}
|
|
else if (m_WarmodeChanges++ == WarmodeCatchCount)
|
|
{
|
|
Timer.StartTimer(WarmodeSpamDelay, WarmodeSpamTimeout);
|
|
return;
|
|
}
|
|
|
|
Warmode = value;
|
|
}
|
|
|
|
private void WarmodeSpamTimeout()
|
|
{
|
|
Warmode = _warmodeSpamValue;
|
|
m_WarmodeChanges = 0;
|
|
}
|
|
|
|
public bool InLOS(Mobile target) =>
|
|
!Deleted && m_Map != null &&
|
|
(target == this || m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));
|
|
|
|
public bool InLOS(object target) =>
|
|
!Deleted && m_Map != null &&
|
|
(target == this || m_AccessLevel > AccessLevel.Player || target is Item item && item.RootParent == this
|
|
|| m_Map.LineOfSight(this, target));
|
|
|
|
public bool InLOS(Point3D target) =>
|
|
!Deleted && m_Map != null && (m_AccessLevel > AccessLevel.Player || m_Map.LineOfSight(this, target));
|
|
|
|
public bool AtPoint(int x, int y) => m_Location.m_X == x && m_Location.m_Y == y;
|
|
|
|
public bool BeginAction<T>() => BeginAction(typeof(T));
|
|
|
|
public bool BeginAction(object toLock)
|
|
{
|
|
if (_actions == null)
|
|
{
|
|
_actions = new List<object> { toLock };
|
|
return true;
|
|
}
|
|
|
|
if (!_actions.Contains(toLock))
|
|
{
|
|
_actions.Add(toLock);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public bool CanBeginAction<T>() => CanBeginAction(typeof(T));
|
|
|
|
public bool CanBeginAction(object toLock) => _actions?.Contains(toLock) != true;
|
|
|
|
public void EndAction<T>() => EndAction(typeof(T));
|
|
|
|
public void EndAction(object toLock)
|
|
{
|
|
if (_actions != null)
|
|
{
|
|
_actions.Remove(toLock);
|
|
|
|
if (_actions.Count == 0)
|
|
{
|
|
_actions = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual TimeSpan GetLogoutDelay() => Region.GetLogoutDelay(this);
|
|
|
|
private void ExpireParalyzed()
|
|
{
|
|
Paralyzed = false;
|
|
}
|
|
|
|
public virtual void Paralyze(TimeSpan duration)
|
|
{
|
|
if (!m_Paralyzed)
|
|
{
|
|
Paralyzed = true;
|
|
Timer.StartTimer(duration, ExpireParalyzed, out _paraTimerToken);
|
|
}
|
|
}
|
|
|
|
private void ExpireFrozen()
|
|
{
|
|
Frozen = false;
|
|
}
|
|
|
|
public virtual void Freeze(TimeSpan duration)
|
|
{
|
|
if (!m_Frozen)
|
|
{
|
|
Frozen = true;
|
|
Timer.StartTimer(duration, ExpireFrozen, out _frozenTimerToken);
|
|
}
|
|
}
|
|
|
|
public override string ToString() => $"{Serial} \"{Name}\"";
|
|
|
|
public virtual void SendSkillMessage()
|
|
{
|
|
if (NextActionMessage - Core.TickCount >= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NextActionMessage = Core.TickCount + ActionMessageDelay;
|
|
|
|
SendLocalizedMessage(500118); // You must wait a few moments to use another skill.
|
|
}
|
|
|
|
public virtual void SendActionMessage()
|
|
{
|
|
if (NextActionMessage - Core.TickCount >= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
NextActionMessage = Core.TickCount + ActionMessageDelay;
|
|
|
|
SendLocalizedMessage(500119); // You must wait to perform another action.
|
|
}
|
|
|
|
public virtual void ClearHands()
|
|
{
|
|
ClearHand(FindItemOnLayer(Layer.OneHanded));
|
|
ClearHand(FindItemOnLayer(Layer.TwoHanded));
|
|
}
|
|
|
|
public virtual void ClearHand(Item item)
|
|
{
|
|
if (item?.Movable == true && !item.AllowEquippedCast(this))
|
|
{
|
|
var pack = Backpack;
|
|
|
|
if (pack == null)
|
|
{
|
|
AddToBackpack(item);
|
|
}
|
|
else
|
|
{
|
|
pack.DropItem(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Attack(Mobile m)
|
|
{
|
|
if (CheckAttack(m))
|
|
{
|
|
Combatant = m;
|
|
}
|
|
}
|
|
|
|
public virtual bool CheckAttack(Mobile m) => Utility.InUpdateRange(Location, m.Location) && CanSee(m) && InLOS(m);
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Combatant" /> property has changed.
|
|
/// <seealso cref="Combatant" />
|
|
/// </summary>
|
|
public virtual void OnCombatantChange()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Hunger" /> property has changed.
|
|
/// <seealso cref="Hunger" />
|
|
/// </summary>
|
|
public virtual void OnHungerChanged(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Thirst" /> property has changed.
|
|
/// <seealso cref="Thirst" />
|
|
/// </summary>
|
|
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<T>(
|
|
int range, bool allowGround, TargetFlags flags, TargetStateCallback<T> callback,
|
|
T state
|
|
) =>
|
|
Target = new SimpleStateTarget<T>(range, flags, allowGround, callback, state);
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the <see cref="Target">Target property</see> has changed.
|
|
/// </summary>
|
|
protected virtual void OnTargetChange()
|
|
{
|
|
}
|
|
|
|
public virtual bool CheckContextMenuDisplay(IEntity target) => true;
|
|
|
|
private bool InternalOnMove(Direction d)
|
|
{
|
|
if (!OnMove(d))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var e = MovementEventArgs.Create(this, d);
|
|
|
|
EventSink.InvokeMovement(e);
|
|
|
|
var ret = !e.Blocked;
|
|
|
|
e.Free();
|
|
|
|
return ret;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile <see cref="Move">moves</see>.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
protected virtual bool OnMove(Direction d)
|
|
{
|
|
if (m_Hidden && m_AccessLevel == AccessLevel.Player)
|
|
{
|
|
if (AllowedStealthSteps-- <= 0 || (d & Direction.Running) != 0 || Mounted)
|
|
{
|
|
RevealingAction();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CheckMovement(Direction d, out int newZ) => CalcMoves.CheckMovement(this, d, out newZ);
|
|
|
|
private bool CanMove(Direction d, Point3D oldLocation, out Point3D newLocation)
|
|
{
|
|
newLocation = oldLocation;
|
|
|
|
if (m_Spell?.OnCasterMoving(d) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (m_Paralyzed || m_Frozen)
|
|
{
|
|
SendLocalizedMessage(500111); // You are frozen and can not move.
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!CheckMovement(d, out var newZ))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var oldX = oldLocation.m_X;
|
|
var oldY = oldLocation.m_Y;
|
|
var oldZ = oldLocation.m_Z;
|
|
var x = oldX;
|
|
var y = oldY;
|
|
CalcMoves.Offset(d, ref x, ref y);
|
|
newLocation = new Point3D(x, y, newZ);
|
|
|
|
Pushing = false;
|
|
|
|
var map = m_Map;
|
|
|
|
if (map == null || !Region.CanMove(this, d, newLocation, oldLocation, map))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var oldSector = map.GetSector(oldX, oldY);
|
|
var newSector = map.GetSector(x, y);
|
|
|
|
if (oldSector != newSector)
|
|
{
|
|
using var queue = PooledRefQueue<IEntity>.Create(2048);
|
|
foreach (var m in oldSector.Mobiles)
|
|
{
|
|
if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
|
|
{
|
|
queue.Enqueue(m);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOff(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in oldSector.Items)
|
|
{
|
|
if (item.AtWorldPoint(oldX, oldY) &&
|
|
(item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
|
|
{
|
|
queue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOff(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var m in newSector.Mobiles)
|
|
{
|
|
if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
|
|
{
|
|
queue.Enqueue(m);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in newSector.Items)
|
|
{
|
|
if (item.AtWorldPoint(x, y) &&
|
|
(item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
|
|
{
|
|
queue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
if (!queue.Dequeue().OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
using var queue = PooledRefQueue<(IEntity, byte)>.Create(2048);
|
|
foreach (var m in oldSector.Mobiles)
|
|
{
|
|
byte flag;
|
|
if (m != this && m.X == oldX && m.Y == oldY && m.Z + 15 > oldZ && oldZ + 15 > m.Z)
|
|
{
|
|
flag = 1;
|
|
}
|
|
else
|
|
{
|
|
flag = 0;
|
|
}
|
|
|
|
if (m.X == x && m.Y == y && m.Z + 15 > newZ && newZ + 15 > m.Z)
|
|
{
|
|
flag += 2;
|
|
}
|
|
|
|
if (flag > 0)
|
|
{
|
|
queue.Enqueue((m, flag));
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var (entity, flag) = queue.Dequeue();
|
|
|
|
if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
foreach (var item in oldSector.Items)
|
|
{
|
|
byte flag;
|
|
if (item.AtWorldPoint(oldX, oldY) &&
|
|
(item.Z == oldZ || item.Z + item.ItemData.Height > oldZ && oldZ + 15 > item.Z))
|
|
{
|
|
flag = 1;
|
|
}
|
|
else
|
|
{
|
|
flag = 0;
|
|
}
|
|
|
|
if (item.AtWorldPoint(x, y) &&
|
|
(item.Z == newZ || item.Z + item.ItemData.Height > newZ && newZ + 15 > item.Z))
|
|
{
|
|
flag += 2;
|
|
}
|
|
|
|
if (flag > 0)
|
|
{
|
|
queue.Enqueue((item, flag));
|
|
}
|
|
}
|
|
|
|
while (queue.Count > 0)
|
|
{
|
|
var (entity, flag) = queue.Dequeue();
|
|
|
|
if (flag > 0 && !entity.OnMoveOff(this) || flag > 1 && !entity.OnMoveOver(this))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!InternalOnMove(d))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
LastMoveTime = Core.TickCount;
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool Move(Direction d)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
var oldLocation = m_Location;
|
|
Point3D newLocation;
|
|
|
|
if ((m_Direction & Direction.Mask) == (d & Direction.Mask))
|
|
{
|
|
// We are actually moving (not just a direction change)
|
|
if (!CanMove(d, oldLocation, out newLocation))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
DisruptiveAction();
|
|
}
|
|
else
|
|
{
|
|
newLocation = oldLocation;
|
|
}
|
|
|
|
if (m_NetState != null)
|
|
{
|
|
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<IEntity>.Create(2048);
|
|
using var moveClientQueue = PooledRefQueue<Mobile>.Create(2048);
|
|
|
|
foreach (var mob in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (mob == this)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (mob.NetState != null)
|
|
{
|
|
moveClientQueue.Enqueue(mob);
|
|
}
|
|
|
|
moveQueue.Enqueue(mob);
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (item.HandlesOnMovement)
|
|
{
|
|
moveQueue.Enqueue(item);
|
|
}
|
|
}
|
|
|
|
if (moveClientQueue.Count > 0)
|
|
{
|
|
const int cacheLength = OutgoingMobilePackets.MobileMovingPacketCacheByteLength;
|
|
|
|
Span<byte> mobileMovingCache = stackalloc byte[cacheLength];
|
|
mobileMovingCache.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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a Mobile <paramref name="m" /> moves off this Mobile.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
public virtual bool OnMoveOff(Mobile m) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a Mobile <paramref name="m" /> moves over this Mobile.
|
|
/// </summary>
|
|
/// <returns>True if the move is allowed, false if not.</returns>
|
|
public virtual bool OnMoveOver(Mobile m) => m_Map == null || Deleted || m.CheckShove(this);
|
|
|
|
public virtual bool CheckShove(Mobile shoved)
|
|
{
|
|
if ((m_Map.Rules & MapRules.FreeMovement) == 0)
|
|
{
|
|
if (!shoved.Alive || !Alive || shoved.IsDeadBondedPet || IsDeadBondedPet)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (shoved.m_Hidden && shoved.m_AccessLevel > AccessLevel.Player)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!Pushing)
|
|
{
|
|
Pushing = true;
|
|
|
|
int number;
|
|
|
|
if (AccessLevel > AccessLevel.Player)
|
|
{
|
|
number = shoved.m_Hidden ? 1019041 : 1019040;
|
|
}
|
|
else
|
|
{
|
|
if (Stam == StamMax)
|
|
{
|
|
number = shoved.m_Hidden ? 1019043 : 1019042;
|
|
Stam -= 10;
|
|
|
|
RevealingAction();
|
|
}
|
|
else
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
SendLocalizedMessage(number);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile sees another Mobile, <paramref name="m" />, move.
|
|
/// </summary>
|
|
public virtual void OnMovement(Mobile m, Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public virtual void CriminalAction(bool message)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Criminal = true;
|
|
|
|
Region.OnCriminalAction(this, message);
|
|
}
|
|
|
|
public virtual bool IsSnoop(Mobile from) => from != this;
|
|
|
|
/// <summary>
|
|
/// Overridable. Any call to <see cref="Resurrect" /> will silently fail if this method returns false.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual bool CheckResurrect() => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile is <see cref="Resurrect">resurrected</see>.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual void OnBeforeResurrect()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked after the Mobile is <see cref="Resurrect">resurrected</see>.
|
|
/// <seealso cref="Resurrect" />
|
|
/// </summary>
|
|
public virtual void OnAfterResurrect()
|
|
{
|
|
}
|
|
|
|
public virtual void Resurrect()
|
|
{
|
|
if (!Alive)
|
|
{
|
|
if (!Region.OnResurrect(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CheckResurrect())
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnBeforeResurrect();
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
Poison = null;
|
|
|
|
Warmode = false;
|
|
|
|
Hits = 10;
|
|
Stam = StamMax;
|
|
Mana = 0;
|
|
|
|
BodyMod = 0;
|
|
Body = Race.AliveBody(this);
|
|
|
|
ProcessDelta();
|
|
|
|
for (var i = Items.Count - 1; i >= 0; --i)
|
|
{
|
|
if (i >= Items.Count)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var item = Items[i];
|
|
|
|
if (item.ItemID == 0x204E)
|
|
{
|
|
item.Delete();
|
|
}
|
|
}
|
|
|
|
SendIncomingPacket();
|
|
|
|
OnAfterResurrect();
|
|
}
|
|
}
|
|
|
|
public void DropHolding()
|
|
{
|
|
var holding = m_Holding;
|
|
|
|
if (holding != null)
|
|
{
|
|
if (!holding.Deleted && holding.HeldBy == this && holding.Map == Map.Internal)
|
|
{
|
|
AddToBackpack(holding);
|
|
}
|
|
|
|
Holding = null;
|
|
holding.ClearBounce();
|
|
}
|
|
}
|
|
|
|
#if TRACK_LEAKS
|
|
~Mobile()
|
|
{
|
|
EntityFinalizationTracker.NotifyFinalized(this);
|
|
}
|
|
#endif
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked before the Mobile is deleted.
|
|
/// </summary>
|
|
public virtual void OnDelete()
|
|
{
|
|
Spawner?.Remove(this);
|
|
Spawner = null;
|
|
}
|
|
|
|
public virtual bool CheckSpellCast(ISpell spell) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile casts a <paramref name="spell" />.
|
|
/// </summary>
|
|
/// <param name="spell"></param>
|
|
public virtual void OnSpellCast(ISpell spell)
|
|
{
|
|
var now = Core.TickCount;
|
|
if (m_NetState != null && now - m_NetState._nextMovementTime > 0)
|
|
{
|
|
m_NetState._nextMovementTime = now;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after <see cref="TotalWeight" /> changes.
|
|
/// </summary>
|
|
public virtual void OnWeightChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="Skill.Base" /> or <see cref="Skill.BaseFixedPoint" /> property of
|
|
/// <paramref name="skill" /> changes.
|
|
/// </summary>
|
|
public virtual void OnSkillChange(SkillName skill, double oldBase)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Invoked after the mobile is deleted. When overridden, be sure to call the base method.
|
|
/// </summary>
|
|
public virtual void OnAfterDelete()
|
|
{
|
|
StopAggrExpire();
|
|
|
|
CheckAggrExpire();
|
|
|
|
PoisonTimer?.Stop();
|
|
m_HitsTimer?.Stop();
|
|
m_StamTimer?.Stop();
|
|
m_ManaTimer?.Stop();
|
|
_combatTimerToken.Cancel();
|
|
_expireCombatantTimerToken.Cancel();
|
|
_logoutTimerToken.Cancel();
|
|
_expireCriminalTimerToken.Cancel();
|
|
_paraTimerToken.Cancel();
|
|
_frozenTimerToken.Cancel();
|
|
_autoManifestTimerToken.Cancel();
|
|
}
|
|
|
|
public virtual bool AllowSkillUse(SkillName name) => true;
|
|
|
|
public virtual bool UseSkill(SkillName name) => Skills.UseSkill(this, name);
|
|
|
|
public virtual bool UseSkill(int skillID) => Skills.UseSkill(this, skillID);
|
|
|
|
public virtual DeathMoveResult GetParentMoveResultFor(Item item) => item.OnParentDeath(this);
|
|
|
|
public virtual DeathMoveResult GetInventoryMoveResultFor(Item item) => item.OnInventoryDeath(this);
|
|
|
|
public virtual void Kill()
|
|
{
|
|
if (!CanBeDamaged())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Alive || IsDeadBondedPet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnBeforeDeath(this))
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!OnBeforeDeath())
|
|
{
|
|
return;
|
|
}
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
m_NetState?.CancelAllTrades();
|
|
|
|
m_Spell?.OnCasterKilled();
|
|
// m_Spell.Disturb( DisturbType.Kill );
|
|
|
|
m_Target?.Cancel(this, TargetCancelType.Canceled);
|
|
|
|
DisruptiveAction();
|
|
|
|
Warmode = false;
|
|
|
|
DropHolding();
|
|
|
|
Hits = 0;
|
|
Stam = 0;
|
|
Mana = 0;
|
|
|
|
Poison = null;
|
|
Combatant = null;
|
|
|
|
if (Paralyzed)
|
|
{
|
|
Paralyzed = false;
|
|
}
|
|
|
|
if (Frozen)
|
|
{
|
|
Frozen = false;
|
|
}
|
|
|
|
var content = new List<Item>();
|
|
var equip = new List<Item>();
|
|
var moveToPack = new List<Item>();
|
|
|
|
var itemsCopy = new List<Item>(Items);
|
|
|
|
var pack = Backpack;
|
|
|
|
for (var i = 0; i < itemsCopy.Count; ++i)
|
|
{
|
|
var item = itemsCopy[i];
|
|
|
|
if (item == pack)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var res = GetParentMoveResultFor(item);
|
|
|
|
switch (res)
|
|
{
|
|
case DeathMoveResult.MoveToCorpse:
|
|
{
|
|
content.Add(item);
|
|
equip.Add(item);
|
|
break;
|
|
}
|
|
case DeathMoveResult.MoveToBackpack:
|
|
{
|
|
moveToPack.Add(item);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (pack != null)
|
|
{
|
|
var packCopy = new List<Item>(pack.Items);
|
|
|
|
for (var i = 0; i < packCopy.Count; ++i)
|
|
{
|
|
var item = packCopy[i];
|
|
|
|
var res = GetInventoryMoveResultFor(item);
|
|
|
|
if (res == DeathMoveResult.MoveToCorpse)
|
|
{
|
|
content.Add(item);
|
|
}
|
|
else
|
|
{
|
|
moveToPack.Add(item);
|
|
}
|
|
}
|
|
|
|
for (var i = 0; i < moveToPack.Count; ++i)
|
|
{
|
|
var item = moveToPack[i];
|
|
|
|
if (RetainPackLocsOnDeath && item.Parent == pack)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
pack.DropItem(item);
|
|
}
|
|
}
|
|
|
|
var c = CreateCorpseHandler?.Invoke(this, content, equip);
|
|
|
|
if (m_Map != null)
|
|
{
|
|
var corpseSerial = c?.Serial ?? Serial.Zero;
|
|
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile is <see cref="Kill">killed</see>.
|
|
/// <seealso cref="Kill" />
|
|
/// <seealso cref="OnDeath" />
|
|
/// </summary>
|
|
/// <returns>True to continue with death, false to override it.</returns>
|
|
public virtual bool OnBeforeDeath() => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked after the Mobile is <see cref="Kill">killed</see>. Primarily, this method is responsible for
|
|
/// deleting an NPC or turning a PC into a ghost.
|
|
/// <seealso cref="Kill" />
|
|
/// <seealso cref="OnBeforeDeath" />
|
|
/// </summary>
|
|
public virtual void OnDeath(Container c)
|
|
{
|
|
var sound = GetDeathSound();
|
|
|
|
if (sound >= 0)
|
|
{
|
|
Effects.PlaySound(this, sound);
|
|
}
|
|
|
|
if (!m_Player)
|
|
{
|
|
Delete();
|
|
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>(T oldItem, int amount) where T : Item
|
|
{
|
|
T item;
|
|
try
|
|
{
|
|
item = oldItem.GetType().CreateInstance<T>();
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Warning(
|
|
e,
|
|
"[{Serial} {Name}]: Item must have a zero parameter constructor to be separated from a stack.",
|
|
oldItem.Serial,
|
|
oldItem.GetType().Name
|
|
);
|
|
|
|
return null;
|
|
}
|
|
|
|
var oldAmount = oldItem.Amount;
|
|
oldItem.Amount = amount;
|
|
|
|
item.Visible = oldItem.Visible;
|
|
item.Movable = oldItem.Movable;
|
|
item.LootType = oldItem.LootType;
|
|
item.Direction = oldItem.Direction;
|
|
item.Hue = oldItem.Hue;
|
|
item.ItemID = oldItem.ItemID;
|
|
item.Location = oldItem.Location;
|
|
item.Layer = oldItem.Layer;
|
|
item.Name = oldItem.Name;
|
|
item.Weight = oldItem.Weight;
|
|
|
|
item.Amount = oldAmount - amount;
|
|
item.Map = oldItem.Map;
|
|
|
|
oldItem.OnAfterDuped(item);
|
|
|
|
if (oldItem.Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.AddItem(item);
|
|
}
|
|
else if (oldItem.Parent is Item parentItem)
|
|
{
|
|
parentItem.AddItem(item);
|
|
}
|
|
|
|
item.Delta(ItemDelta.Update);
|
|
|
|
return item;
|
|
}
|
|
|
|
public virtual void SendDropEffect(Item item)
|
|
{
|
|
if (!DragEffects || item.Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var map = m_Map;
|
|
var root = item.RootParent;
|
|
var rootItem = root as Item;
|
|
|
|
if (map == null || root != null && rootItem == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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<Mobile> hears, ref string text, ref object context)
|
|
{
|
|
if (Alive)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
ReadOnlySpan<char> ghostChars = (GhostChars ?? DefaultGhostChars).AsSpan();
|
|
|
|
var length = text.Length;
|
|
char[] rentedChars = null;
|
|
var chars = length <= 256
|
|
? stackalloc char[length]
|
|
: rentedChars = STArrayPool<char>.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<char>.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<IEntity> list, Container cont)
|
|
{
|
|
for (var i = 0; i < cont.Items.Count; ++i)
|
|
{
|
|
var item = cont.Items[i];
|
|
|
|
if (item.HandlesOnSpeech)
|
|
{
|
|
list.Add(item);
|
|
}
|
|
|
|
if (item is Container container)
|
|
{
|
|
AddSpeechItemsFrom(list, container);
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void DoSpeech(string text, int[] keywords, MessageType type, int hue)
|
|
{
|
|
if (Deleted || CommandSystem.Handle(this, text, type))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var range = 15;
|
|
|
|
switch (type)
|
|
{
|
|
case MessageType.Regular:
|
|
{
|
|
SpeechHue = hue;
|
|
break;
|
|
}
|
|
case MessageType.Emote:
|
|
{
|
|
EmoteHue = hue;
|
|
break;
|
|
}
|
|
case MessageType.Whisper:
|
|
{
|
|
WhisperHue = hue;
|
|
range = 1;
|
|
break;
|
|
}
|
|
case MessageType.Yell:
|
|
{
|
|
YellHue = hue;
|
|
range = 18;
|
|
break;
|
|
}
|
|
case MessageType.System:
|
|
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<int>());
|
|
}
|
|
|
|
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>();
|
|
}
|
|
|
|
DamageEntry resp = null;
|
|
foreach (var check in list)
|
|
{
|
|
if (check.Damager == master)
|
|
{
|
|
resp = check;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (resp == null)
|
|
{
|
|
list.Add(resp = new DamageEntry(master));
|
|
}
|
|
|
|
resp.DamageGiven += amount;
|
|
resp.LastDamage = Core.Now;
|
|
}
|
|
|
|
return de;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is <see cref="Damage">damaged</see>. It is called before
|
|
/// <see cref="Hits">hit points</see> are lowered or the Mobile is <see cref="Kill">killed</see>.
|
|
/// <seealso cref="Damage" />
|
|
/// <seealso cref="Hits" />
|
|
/// <seealso cref="Kill" />
|
|
/// </summary>
|
|
public virtual void OnDamage(int amount, Mobile from, bool willKill)
|
|
{
|
|
}
|
|
|
|
public virtual bool CanBeDamaged() => !m_Blessed;
|
|
|
|
public virtual void Damage(int amount, Mobile from = null, bool informMount = true, bool ignoreEvilOmen = false)
|
|
{
|
|
if (amount <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!CanBeDamaged() || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnDamage(this, ref amount))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldHits = Hits;
|
|
var newHits = oldHits - amount;
|
|
|
|
m_Spell?.OnCasterHurt();
|
|
|
|
// if (m_Spell != null && m_Spell.State == SpellState.Casting)
|
|
// m_Spell.Disturb( DisturbType.Hurt, false, true );
|
|
|
|
if (from != null)
|
|
{
|
|
RegisterDamage(amount, from);
|
|
}
|
|
|
|
DisruptiveAction();
|
|
|
|
Paralyzed = false;
|
|
|
|
switch (VisibleDamageType)
|
|
{
|
|
case VisibleDamageType.Related:
|
|
{
|
|
SendVisibleDamageRelated(from, amount);
|
|
break;
|
|
}
|
|
case VisibleDamageType.Everyone:
|
|
{
|
|
SendVisibleDamageEveryone(amount);
|
|
break;
|
|
}
|
|
case VisibleDamageType.Selective:
|
|
{
|
|
SendVisibleDamageSelective(from, amount);
|
|
break;
|
|
}
|
|
}
|
|
|
|
OnDamage(amount, from, newHits < 0);
|
|
|
|
if (informMount)
|
|
{
|
|
Mount?.OnRiderDamaged(amount, from, newHits < 0);
|
|
}
|
|
|
|
if (newHits < 0)
|
|
{
|
|
LastKiller = from;
|
|
|
|
Hits = 0;
|
|
|
|
if (oldHits >= 0)
|
|
{
|
|
Kill();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Hits = newHits;
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageRelated(Mobile from, int amount)
|
|
{
|
|
var ourState = m_NetState ?? GetDamageMaster(from)?.m_NetState;
|
|
var theirState = from?.m_NetState ?? from?.GetDamageMaster(this)?.m_NetState;
|
|
|
|
if (amount > 0)
|
|
{
|
|
ourState.SendDamage(Serial, amount);
|
|
|
|
if (theirState != null && theirState != ourState)
|
|
{
|
|
theirState.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageEveryone(int amount)
|
|
{
|
|
if (amount < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var map = m_Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var ns in map.GetClientsInRange(m_Location))
|
|
{
|
|
if (ns.Mobile.CanSee(this))
|
|
{
|
|
ns.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void SendVisibleDamageSelective(Mobile from, int amount)
|
|
{
|
|
var ourState = m_NetState;
|
|
var theirState = from?.m_NetState;
|
|
|
|
var damager = from;
|
|
var damaged = this;
|
|
|
|
if (ourState == null)
|
|
{
|
|
var master = GetDamageMaster(from);
|
|
|
|
if (master != null)
|
|
{
|
|
damaged = master;
|
|
ourState = master.m_NetState;
|
|
}
|
|
}
|
|
|
|
if (!damaged.ShowVisibleDamage)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (theirState == null && from != null)
|
|
{
|
|
var master = from.GetDamageMaster(this);
|
|
|
|
if (master != null)
|
|
{
|
|
damager = master;
|
|
theirState = master.m_NetState;
|
|
}
|
|
}
|
|
|
|
if (amount > 0)
|
|
{
|
|
if (damaged.CanSeeVisibleDamage)
|
|
{
|
|
ourState.SendDamage(Serial, amount);
|
|
}
|
|
|
|
if (theirState != null && theirState != ourState && damager.CanSeeVisibleDamage)
|
|
{
|
|
theirState.SendDamage(Serial, amount);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Heal(int amount) => Heal(amount, this);
|
|
|
|
public void Heal(int amount, Mobile from, bool message = true)
|
|
{
|
|
if (!Alive || IsDeadBondedPet)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!Region.OnHeal(this, ref amount))
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnHeal(ref amount, from);
|
|
|
|
if (Hits + amount > HitsMax)
|
|
{
|
|
amount = HitsMax - Hits;
|
|
}
|
|
|
|
Hits += amount;
|
|
|
|
if (message && amount > 0)
|
|
{
|
|
m_NetState.SendMessageLocalizedAffix(
|
|
Serial.MinusOne,
|
|
-1,
|
|
MessageType.Label,
|
|
0x3B2,
|
|
3,
|
|
1008158,
|
|
"",
|
|
AffixType.Append | AffixType.System,
|
|
amount.ToString()
|
|
);
|
|
}
|
|
}
|
|
|
|
public virtual void OnHeal(ref int amount, Mobile from)
|
|
{
|
|
}
|
|
|
|
public virtual void Deserialize(IGenericReader reader)
|
|
{
|
|
var version = reader.ReadInt();
|
|
|
|
switch (version)
|
|
{
|
|
case 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:
|
|
{
|
|
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<Container>();
|
|
|
|
goto case 23;
|
|
}
|
|
case 23:
|
|
{
|
|
if (version < 33)
|
|
{
|
|
Created = reader.ReadDateTime();
|
|
}
|
|
|
|
goto case 22;
|
|
}
|
|
case 22: // Just removed followers
|
|
case 21:
|
|
{
|
|
if (version < 34)
|
|
{
|
|
// Migrated to PlayerMobile
|
|
AddToStabledMigration(this, reader.ReadEntitySet<Mobile>(true));
|
|
}
|
|
|
|
goto case 20;
|
|
}
|
|
case 20:
|
|
{
|
|
CantWalk = reader.ReadBool();
|
|
|
|
goto case 19;
|
|
}
|
|
case 19: // Just removed variables
|
|
case 18:
|
|
{
|
|
if (version < 36)
|
|
{
|
|
reader.ReadByte(); // VirtueInfo version
|
|
|
|
int mask = reader.ReadByte();
|
|
|
|
if (mask != 0)
|
|
{
|
|
var virtueValues = new int[8];
|
|
|
|
for (var i = 0; i < 8; ++i)
|
|
{
|
|
if ((mask & (1 << i)) != 0)
|
|
{
|
|
virtueValues[i] = reader.ReadInt();
|
|
}
|
|
}
|
|
|
|
AddToVirtueMigration(this, virtueValues);
|
|
}
|
|
}
|
|
|
|
goto case 17;
|
|
}
|
|
case 17:
|
|
{
|
|
Thirst = reader.ReadInt();
|
|
BAC = reader.ReadInt();
|
|
|
|
goto case 16;
|
|
}
|
|
case 16:
|
|
{
|
|
if (version < 35)
|
|
{
|
|
// Migrated to PlayerMurderSystem
|
|
AddToMurderMigration(this, reader.ReadInt());
|
|
}
|
|
|
|
if (version <= 24)
|
|
{
|
|
reader.ReadDateTime();
|
|
reader.ReadDateTime();
|
|
}
|
|
|
|
goto case 15;
|
|
}
|
|
case 15:
|
|
{
|
|
if (version < 22)
|
|
{
|
|
reader.ReadInt(); // followers
|
|
}
|
|
|
|
m_FollowersMax = reader.ReadInt();
|
|
|
|
goto case 14;
|
|
}
|
|
case 14:
|
|
{
|
|
MagicDamageAbsorb = reader.ReadInt();
|
|
|
|
goto case 13;
|
|
}
|
|
case 13:
|
|
{
|
|
GuildFealty = reader.ReadEntity<Mobile>();
|
|
|
|
goto case 12;
|
|
}
|
|
case 12:
|
|
{
|
|
m_Guild = reader.ReadEntity<BaseGuild>();
|
|
|
|
goto case 11;
|
|
}
|
|
case 11:
|
|
{
|
|
m_DisplayGuildTitle = reader.ReadBool();
|
|
|
|
goto case 10;
|
|
}
|
|
case 10:
|
|
{
|
|
CanSwim = reader.ReadBool();
|
|
|
|
goto case 9;
|
|
}
|
|
case 9:
|
|
{
|
|
Squelched = reader.ReadBool();
|
|
|
|
goto case 8;
|
|
}
|
|
case 8:
|
|
{
|
|
m_Holding = reader.ReadEntity<Item>();
|
|
|
|
goto case 7;
|
|
}
|
|
case 7:
|
|
{
|
|
m_VirtualArmor = reader.ReadInt();
|
|
|
|
goto case 6;
|
|
}
|
|
case 6:
|
|
{
|
|
BaseSoundID = reader.ReadInt();
|
|
|
|
goto case 5;
|
|
}
|
|
case 5:
|
|
{
|
|
DisarmReady = reader.ReadBool();
|
|
StunReady = reader.ReadBool();
|
|
|
|
goto case 4;
|
|
}
|
|
case 4:
|
|
{
|
|
if (version <= 25)
|
|
{
|
|
reader.ReadPoison();
|
|
}
|
|
|
|
goto case 3;
|
|
}
|
|
case 3:
|
|
{
|
|
m_StatCap = reader.ReadInt();
|
|
|
|
goto case 2;
|
|
}
|
|
case 2:
|
|
{
|
|
NameHue = reader.ReadInt();
|
|
|
|
goto case 1;
|
|
}
|
|
case 1:
|
|
{
|
|
m_Hunger = reader.ReadInt();
|
|
|
|
goto case 0;
|
|
}
|
|
case 0:
|
|
{
|
|
if (version < 11)
|
|
{
|
|
m_DisplayGuildTitle = true;
|
|
}
|
|
|
|
if (version < 3)
|
|
{
|
|
m_StatCap = 225;
|
|
}
|
|
|
|
if (version < 15)
|
|
{
|
|
m_Followers = 0;
|
|
m_FollowersMax = 5;
|
|
}
|
|
|
|
m_Location = reader.ReadPoint3D();
|
|
m_Body = new Body(reader.ReadInt());
|
|
m_Name = reader.ReadString();
|
|
m_GuildTitle = reader.ReadString();
|
|
m_Criminal = reader.ReadBool();
|
|
m_Kills = reader.ReadInt();
|
|
SpeechHue = reader.ReadInt();
|
|
EmoteHue = reader.ReadInt();
|
|
WhisperHue = reader.ReadInt();
|
|
YellHue = reader.ReadInt();
|
|
m_Language = reader.ReadString();
|
|
m_Female = reader.ReadBool();
|
|
m_Warmode = reader.ReadBool();
|
|
m_Hidden = reader.ReadBool();
|
|
m_Direction = (Direction)reader.ReadByte();
|
|
m_Hue = reader.ReadInt();
|
|
m_Str = reader.ReadInt();
|
|
m_Dex = reader.ReadInt();
|
|
m_Int = reader.ReadInt();
|
|
m_Hits = reader.ReadInt();
|
|
m_Stam = reader.ReadInt();
|
|
m_Mana = reader.ReadInt();
|
|
m_Map = reader.ReadMap();
|
|
m_Blessed = reader.ReadBool();
|
|
m_Fame = reader.ReadInt();
|
|
m_Karma = reader.ReadInt();
|
|
m_AccessLevel = (AccessLevel)reader.ReadByte();
|
|
|
|
Skills = new Skills(this, reader);
|
|
|
|
Items = reader.ReadEntityList<Item>();
|
|
|
|
m_Player = reader.ReadBool();
|
|
m_Title = reader.ReadString();
|
|
Profile = reader.ReadString();
|
|
ProfileLocked = reader.ReadBool();
|
|
|
|
if (version <= 18)
|
|
{
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
reader.ReadInt();
|
|
}
|
|
|
|
AutoPageNotify = reader.ReadBool();
|
|
|
|
LogoutLocation = reader.ReadPoint3D();
|
|
LogoutMap = reader.ReadMap();
|
|
|
|
m_StrLock = (StatLockType)reader.ReadByte();
|
|
m_DexLock = (StatLockType)reader.ReadByte();
|
|
m_IntLock = (StatLockType)reader.ReadByte();
|
|
|
|
_statMods = new List<StatMod>();
|
|
_skillMods = new List<SkillMod>();
|
|
|
|
if (version < 32)
|
|
{
|
|
if (reader.ReadBool())
|
|
{
|
|
var count = reader.ReadInt();
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
reader.ReadDateTime();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (m_Player && m_Map != Map.Internal)
|
|
{
|
|
LogoutLocation = m_Location;
|
|
LogoutMap = m_Map;
|
|
|
|
m_Map = Map.Internal;
|
|
}
|
|
|
|
m_Map?.OnEnter(this);
|
|
|
|
if (m_Criminal)
|
|
{
|
|
Timer.StartTimer(ExpireCriminalDelay, ExpireCriminal, out _expireCriminalTimerToken);
|
|
}
|
|
|
|
if (ShouldCheckStatTimers)
|
|
{
|
|
CheckStatTimers();
|
|
}
|
|
|
|
UpdateRegion();
|
|
|
|
UpdateResistances();
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!m_Player)
|
|
{
|
|
Utility.Intern(ref m_Name);
|
|
}
|
|
|
|
Utility.Intern(ref m_Title);
|
|
Utility.Intern(ref m_Language);
|
|
}
|
|
|
|
public void ConvertHair()
|
|
{
|
|
Item hair;
|
|
|
|
if ((hair = FindItemOnLayer(Layer.Hair)) != null)
|
|
{
|
|
HairItemID = hair.ItemID;
|
|
HairHue = hair.Hue;
|
|
hair.Delete();
|
|
}
|
|
|
|
if ((hair = FindItemOnLayer(Layer.FacialHair)) != null)
|
|
{
|
|
FacialHairItemID = hair.ItemID;
|
|
FacialHairHue = hair.Hue;
|
|
hair.Delete();
|
|
}
|
|
}
|
|
|
|
public virtual void CheckStatTimers()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Hits < HitsMax)
|
|
{
|
|
if (CanRegenHits)
|
|
{
|
|
m_HitsTimer ??= new HitsTimer(this);
|
|
m_HitsTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_HitsTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Hits = HitsMax;
|
|
}
|
|
|
|
if (Stam < StamMax)
|
|
{
|
|
if (CanRegenStam)
|
|
{
|
|
m_StamTimer ??= new StamTimer(this);
|
|
m_StamTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_StamTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Stam = StamMax;
|
|
}
|
|
|
|
if (Mana < ManaMax)
|
|
{
|
|
if (CanRegenMana)
|
|
{
|
|
m_ManaTimer ??= new ManaTimer(this);
|
|
m_ManaTimer.Start();
|
|
}
|
|
else
|
|
{
|
|
m_ManaTimer?.Stop();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Mana = ManaMax;
|
|
}
|
|
}
|
|
|
|
public static string GetAccessLevelName(AccessLevel level) => m_AccessLevelNames[(int)level];
|
|
|
|
public virtual bool CanPaperdollBeOpenedBy(Mobile from) => Body.IsHuman || Body.IsGhost || IsBodyMod;
|
|
|
|
public virtual void GetChildContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
|
|
{
|
|
}
|
|
|
|
public void Internalize()
|
|
{
|
|
Map = Map.Internal;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="AddItem">added</see> from the Mobile,
|
|
/// such
|
|
/// as when it is equipped.
|
|
/// <seealso cref="Items" />
|
|
/// <seealso cref="OnItemRemoved" />
|
|
/// </summary>
|
|
public virtual void OnItemAdded(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is <see cref="RemoveItem">removed</see> from the
|
|
/// Mobile.
|
|
/// <seealso cref="Items" />
|
|
/// <seealso cref="OnItemAdded" />
|
|
/// </summary>
|
|
public virtual void OnItemRemoved(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is becomes a child of the Mobile; it's worn or
|
|
/// contained
|
|
/// at some level of the Mobile's <see cref="Mobile.Backpack">backpack</see> or <see cref="Mobile.BankBox">bank box</see>
|
|
/// <seealso cref="OnSubItemRemoved" />
|
|
/// <seealso cref="OnItemAdded" />
|
|
/// </summary>
|
|
public virtual void OnSubItemAdded(Item item)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="item" /> is removed from the Mobile, its
|
|
/// <see cref="Mobile.Backpack">backpack</see>, or its <see cref="Mobile.BankBox">bank box</see>.
|
|
/// <seealso cref="OnSubItemAdded" />
|
|
/// <seealso cref="OnItemRemoved" />
|
|
/// </summary>
|
|
public virtual void OnSubItemRemoved(Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void OnItemBounceCleared(Item item)
|
|
{
|
|
}
|
|
|
|
public virtual void OnSubItemBounceCleared(Item item)
|
|
{
|
|
}
|
|
|
|
public void AddItem(Item item)
|
|
{
|
|
if (item?.Deleted != false)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item.Parent == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (item.Parent is Mobile parentMobile)
|
|
{
|
|
parentMobile.RemoveItem(item);
|
|
}
|
|
else if (item.Parent is Item parentItem)
|
|
{
|
|
parentItem.RemoveItem(item);
|
|
}
|
|
else
|
|
{
|
|
item.SendRemovePacket();
|
|
}
|
|
|
|
item.Parent = this;
|
|
item.Map = m_Map;
|
|
|
|
Items.Add(item);
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, item.TotalItems + 1);
|
|
UpdateTotal(item, TotalType.Weight, item.TotalWeight + item.PileWeight);
|
|
}
|
|
|
|
item.Delta(ItemDelta.Update);
|
|
|
|
item.OnAdded(this);
|
|
OnItemAdded(item);
|
|
|
|
if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
|
|
item.PoisonResistance != 0 || item.EnergyResistance != 0)
|
|
{
|
|
UpdateResistances();
|
|
}
|
|
}
|
|
|
|
public void RemoveItem(Item item)
|
|
{
|
|
if (item == null || Items == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (Items.Remove(item))
|
|
{
|
|
item.SendRemovePacket();
|
|
|
|
if (!item.IsVirtualItem)
|
|
{
|
|
UpdateTotal(item, TotalType.Gold, -item.TotalGold);
|
|
UpdateTotal(item, TotalType.Items, -(item.TotalItems + 1));
|
|
UpdateTotal(item, TotalType.Weight, -(item.TotalWeight + item.PileWeight));
|
|
}
|
|
|
|
item.Parent = null;
|
|
|
|
item.OnRemoved(this);
|
|
OnItemRemoved(item);
|
|
|
|
if (item.PhysicalResistance != 0 || item.FireResistance != 0 || item.ColdResistance != 0 ||
|
|
item.PoisonResistance != 0 || item.EnergyResistance != 0)
|
|
{
|
|
UpdateResistances();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void Animate(int action, int frameCount, int repeatCount, bool forward, bool repeat, int delay)
|
|
{
|
|
var map = m_Map;
|
|
|
|
if (map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
ProcessDelta();
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked before the Mobile says something.
|
|
/// <seealso cref="DoSpeech" />
|
|
/// </summary>
|
|
public virtual void OnSaid(SpeechEventArgs e)
|
|
{
|
|
if (Squelched)
|
|
{
|
|
if (Core.ML)
|
|
{
|
|
SendLocalizedMessage(500168); // You can not say anything, you have been muted.
|
|
}
|
|
else
|
|
{
|
|
SendMessage("You can not say anything, you have been squelched."); // Cliloc ITSELF changed during ML.
|
|
}
|
|
|
|
e.Blocked = true;
|
|
}
|
|
|
|
if (!e.Blocked)
|
|
{
|
|
RevealingAction();
|
|
}
|
|
}
|
|
|
|
public virtual bool HandlesOnSpeech(Mobile from) => false;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile hears speech. This event will only be invoked if
|
|
/// <see cref="HandlesOnSpeech" /> returns true.
|
|
/// <seealso cref="DoSpeech" />
|
|
/// </summary>
|
|
public virtual void OnSpeech(SpeechEventArgs e)
|
|
{
|
|
}
|
|
|
|
public void SendEverything()
|
|
{
|
|
var ns = m_NetState;
|
|
|
|
if (m_Map == null || ns == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var m in m_Map.GetMobilesInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (CanSee(m) && Utility.InUpdateRange(m_Location, m.m_Location))
|
|
{
|
|
ns.SendMobileIncoming(this, m);
|
|
|
|
if (ns.StygianAbyss)
|
|
{
|
|
ns.SendMobileHealthbar(m, Healthbar.Yellow);
|
|
ns.SendMobileHealthbar(m, Healthbar.Poison);
|
|
}
|
|
|
|
if (m.IsDeadBondedPet)
|
|
{
|
|
ns.SendBondedStatus(m.Serial, true);
|
|
}
|
|
|
|
m.SendOPLPacketTo(ns);
|
|
}
|
|
}
|
|
|
|
foreach (var item in m_Map.GetItemsInRange(m_Location, Core.GlobalMaxUpdateRange))
|
|
{
|
|
if (CanSee(item) && InRange(item.Location, item.GetUpdateRange(this)))
|
|
{
|
|
item.SendInfoTo(ns);
|
|
}
|
|
}
|
|
|
|
var range = new Rectangle2D(
|
|
m_Location.X - Core.GlobalMaxUpdateRange,
|
|
m_Location.Y - Core.GlobalMaxUpdateRange,
|
|
Core.GlobalMaxUpdateRange * 2 + 1,
|
|
Core.GlobalMaxUpdateRange * 2 + 1
|
|
);
|
|
|
|
foreach (var multi in m_Map.GetMultisInBounds(range))
|
|
{
|
|
multi.SendInfoTo(ns);
|
|
}
|
|
}
|
|
|
|
public void UpdateRegion()
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var newRegion = Region.Find(m_Location, m_Map);
|
|
|
|
if (newRegion != m_Region)
|
|
{
|
|
Region.OnRegionChange(this, m_Region, newRegion);
|
|
|
|
var oldRegion = m_Region;
|
|
m_Region = newRegion;
|
|
OnRegionChange(oldRegion, newRegion);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Map" /> changes.
|
|
/// </summary>
|
|
protected virtual void OnMapChange(Map oldMap)
|
|
{
|
|
}
|
|
|
|
public void SetDirection(Direction dir)
|
|
{
|
|
m_Direction = dir;
|
|
}
|
|
|
|
public virtual int GetSeason() => m_Map?.Season ?? 1;
|
|
|
|
public virtual int GetPacketFlags(bool stygianAbyss)
|
|
{
|
|
var flags = 0x0;
|
|
|
|
if (m_Paralyzed || m_Frozen || _disableCastParalyze && m_Spell?.BlocksMovement == true)
|
|
{
|
|
flags |= 0x01;
|
|
}
|
|
|
|
if (m_Female)
|
|
{
|
|
flags |= 0x02;
|
|
}
|
|
|
|
if (stygianAbyss)
|
|
{
|
|
if (m_Flying)
|
|
{
|
|
flags |= 0x04;
|
|
}
|
|
}
|
|
else if (m_Poison != null)
|
|
{
|
|
flags |= 0x04;
|
|
}
|
|
|
|
if (m_Blessed || m_YellowHealthbar)
|
|
{
|
|
flags |= 0x08;
|
|
}
|
|
|
|
if (IgnoreMobiles)
|
|
{
|
|
flags |= 0x10;
|
|
}
|
|
|
|
if (m_Warmode)
|
|
{
|
|
flags |= 0x40;
|
|
}
|
|
|
|
if (m_Hidden)
|
|
{
|
|
flags |= 0x80;
|
|
}
|
|
|
|
return flags;
|
|
}
|
|
|
|
public virtual void OnGenderChanged(bool oldFemale)
|
|
{
|
|
}
|
|
|
|
public virtual void ToggleFlying()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked after the Warmode property has changed.
|
|
/// </summary>
|
|
public virtual void OnWarmodeChanged()
|
|
{
|
|
}
|
|
|
|
public virtual void OnHiddenChanged()
|
|
{
|
|
AllowedStealthSteps = 0;
|
|
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
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)
|
|
{
|
|
GetProperties(list);
|
|
list.Terminate();
|
|
return list;
|
|
}
|
|
|
|
public void ClearProperties()
|
|
{
|
|
m_PropertyList = null;
|
|
}
|
|
|
|
#nullable enable
|
|
public void InvalidateProperties()
|
|
{
|
|
if (!ObjectPropertyList.Enabled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (m_Map != null && m_Map != Map.Internal && !World.Loading)
|
|
{
|
|
int? oldHash;
|
|
int newHash;
|
|
if (m_PropertyList != null)
|
|
{
|
|
oldHash = m_PropertyList.Hash;
|
|
m_PropertyList.Reset();
|
|
InitializePropertyList(m_PropertyList);
|
|
newHash = m_PropertyList.Hash;
|
|
}
|
|
else
|
|
{
|
|
oldHash = null;
|
|
newHash = PropertyList.Hash;
|
|
}
|
|
|
|
if (oldHash != newHash)
|
|
{
|
|
Delta(MobileDelta.Properties);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
ClearProperties();
|
|
}
|
|
}
|
|
#nullable restore
|
|
|
|
public virtual void SetLocation(Point3D newLocation, bool isTeleport)
|
|
{
|
|
if (Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var oldLocation = m_Location;
|
|
|
|
if (oldLocation == newLocation)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_Location = newLocation;
|
|
m_Map?.OnMove(oldLocation, this);
|
|
UpdateRegion();
|
|
|
|
var box = FindBankNoCreate();
|
|
|
|
if (box?.Opened == true)
|
|
{
|
|
box.Close();
|
|
}
|
|
|
|
if (m_NetState != null)
|
|
{
|
|
m_NetState.ValidateAllTrades();
|
|
|
|
if (isTeleport && (!m_NetState.HighSeas || !NoMoveHS))
|
|
{
|
|
m_NetState.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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Location" /> changes.
|
|
/// </summary>
|
|
protected virtual void OnLocationChange(Point3D oldLocation)
|
|
{
|
|
}
|
|
|
|
public bool HasFreeHand() => FindItemOnLayer(Layer.TwoHanded) == null;
|
|
|
|
public virtual IWeapon GetDefaultWeapon() => DefaultWeapon;
|
|
|
|
public BankBox FindBankNoCreate()
|
|
{
|
|
if (m_BankBox?.Deleted != false || m_BankBox.Parent != this)
|
|
{
|
|
m_BankBox = FindItemOnLayer<BankBox>(Layer.Bank);
|
|
}
|
|
|
|
return m_BankBox;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public T FindItemOnLayer<T>(Layer layer) where T : Item => FindItemOnLayer(layer) as T;
|
|
|
|
public Item FindItemOnLayer(Layer layer)
|
|
{
|
|
var eq = Items;
|
|
var count = eq.Count;
|
|
|
|
for (var i = 0; i < count; ++i)
|
|
{
|
|
var item = eq[i];
|
|
|
|
// TODO: We only allow 1 item per layer. It's an implicit contract.
|
|
if (!item.Deleted && item.Layer == layer)
|
|
{
|
|
return item;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public void SendIncomingPacket()
|
|
{
|
|
if (m_Map == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var state in m_Map.GetClientsInRange(m_Location))
|
|
{
|
|
var m = state.Mobile;
|
|
if (m.CanSee(this))
|
|
{
|
|
state.SendMobileIncoming(m, this);
|
|
|
|
if (state.StygianAbyss)
|
|
{
|
|
state.SendMobileHealthbar(this, Healthbar.Poison);
|
|
state.SendMobileHealthbar(this, Healthbar.Yellow);
|
|
}
|
|
|
|
if (IsDeadBondedPet)
|
|
{
|
|
state.SendBondedStatus(Serial, true);
|
|
}
|
|
|
|
SendOPLPacketTo(state);
|
|
}
|
|
}
|
|
}
|
|
|
|
public bool PlaceInBackpack(Item item) =>
|
|
!item.Deleted && Backpack?.TryDropItem(this, item, false) == true;
|
|
|
|
public bool AddToBackpack(Item item)
|
|
{
|
|
if (item.Deleted)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!PlaceInBackpack(item))
|
|
{
|
|
var loc = m_Location;
|
|
var map = m_Map;
|
|
|
|
if ((map == null || map == Map.Internal) && LogoutMap != null)
|
|
{
|
|
loc = LogoutLocation;
|
|
map = LogoutMap;
|
|
}
|
|
|
|
item.MoveToWorld(loc, map);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public virtual bool CheckLift(Mobile from, Item item, ref LRReason reject) => true;
|
|
|
|
public virtual bool CheckNonlocalLift(Mobile from, Item item) =>
|
|
from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;
|
|
|
|
public virtual bool CheckTrade(
|
|
Mobile to, Item item, SecureTradeContainer cont, bool message, bool checkItems, int plusItems, int plusWeight
|
|
) => true;
|
|
|
|
public virtual bool OpenTrade(Mobile from, Item offer = null)
|
|
{
|
|
if (!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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a Mobile (<paramref name="from" />) drops an
|
|
/// <see cref="Item">
|
|
/// <paramref name="dropped" />
|
|
/// </see>
|
|
/// onto the Mobile.
|
|
/// </summary>
|
|
public virtual bool OnDragDrop(Mobile from, Item dropped)
|
|
{
|
|
if (from == this)
|
|
{
|
|
var pack = Backpack;
|
|
return pack != null && dropped.DropToItem(from, pack, new Point3D(-1, -1, 0));
|
|
}
|
|
|
|
return from.InRange(Location, 2) && OpenTrade(from, dropped);
|
|
}
|
|
|
|
public virtual bool CheckEquip(Item item)
|
|
{
|
|
for (var i = 0; i < Items.Count; ++i)
|
|
{
|
|
if (Items[i].CheckConflictingLayer(this, item, item.Layer) ||
|
|
item.CheckConflictingLayer(this, Items[i], Items[i].Layer))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to wear <paramref name="item" />.
|
|
/// </summary>
|
|
/// <returns>True if the request is accepted, false if otherwise.</returns>
|
|
public virtual bool OnEquip(Item item)
|
|
{
|
|
// For some reason OSI allows equipping quest items, but they are unmarked in the process
|
|
if (item.QuestItem)
|
|
{
|
|
item.QuestItem = false;
|
|
|
|
// An item must be in your backpack (and not in a container within) to be toggled as a quest item.
|
|
SendLocalizedMessage(1074769);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to lift <paramref name="item" />.
|
|
/// </summary>
|
|
/// <returns>True if the lift is allowed, false if otherwise.</returns>
|
|
/// <example>
|
|
/// The following example demonstrates usage. It will disallow any attempts to pick up a pick axe if the Mobile does not
|
|
/// have
|
|
/// enough strength.
|
|
/// <code>
|
|
/// public override bool OnDragLift( Item item )
|
|
/// {
|
|
/// if (item is Pickaxe && this.Str < 60)
|
|
/// {
|
|
/// SendMessage( "That is too heavy for you to lift." );
|
|
/// return false;
|
|
/// }
|
|
///
|
|
/// return base.OnDragLift( item );
|
|
/// }</code>
|
|
/// </example>
|
|
public virtual bool OnDragLift(Item item) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into a
|
|
/// <see cref="Container">
|
|
/// <paramref name="container" />
|
|
/// </see>
|
|
/// .
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemInto(Item item, Container container, Point3D loc) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> directly onto another
|
|
/// <see cref="Item" />, <paramref name="target" />. This is the case of stacking items.
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemOnto(Item item, Item target) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> into another
|
|
/// <see cref="Item" />, <paramref name="target" />. The target item is most likely a <see cref="Container" />.
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToItem(Item item, Item target, Point3D loc) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to give <paramref name="item" /> to a Mobile (
|
|
/// <paramref name="target" />).
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToMobile(Item item, Mobile target) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile attempts to drop <paramref name="item" /> to the world at a
|
|
/// <see cref="Point3D">
|
|
/// <paramref name="location" />
|
|
/// </see>
|
|
/// .
|
|
/// </summary>
|
|
/// <returns>True if the drop is allowed, false if otherwise.</returns>
|
|
public virtual bool OnDroppedItemToWorld(Item item, Point3D location) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event when <paramref name="from" /> successfully uses <paramref name="item" /> while it's on this
|
|
/// Mobile.
|
|
/// <seealso cref="Item.OnItemUsed" />
|
|
/// </summary>
|
|
public virtual void OnItemUsed(Mobile from, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual bool CheckNonlocalDrop(Mobile from, Item item, Item target) =>
|
|
from == this || from.AccessLevel > AccessLevel && from.AccessLevel >= AccessLevel.GameMaster;
|
|
|
|
public virtual bool CheckItemUse(Mobile from, Item item) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <paramref name="from" /> successfully lifts <paramref name="item" /> from this
|
|
/// Mobile.
|
|
/// <seealso cref="Item.OnItemLifted" />
|
|
/// </summary>
|
|
public virtual void OnItemLifted(Mobile from, Item item)
|
|
{
|
|
}
|
|
|
|
public virtual bool AllowItemUse(Item item) => true;
|
|
|
|
public virtual bool AllowEquipFrom(Mobile mob) =>
|
|
mob == this || mob.AccessLevel >= AccessLevel.GameMaster && mob.AccessLevel > AccessLevel;
|
|
|
|
public virtual bool EquipItem(Item item)
|
|
{
|
|
if (item?.Deleted != false || !item.CanEquip(this))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (CheckEquip(item) && OnEquip(item) && item.OnEquip(this))
|
|
{
|
|
if (m_Spell?.OnCasterEquipping(item) == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// if (m_Spell != null && m_Spell.State == SpellState.Casting)
|
|
// m_Spell.Disturb( DisturbType.EquipRequest );
|
|
|
|
AddItem(item);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void DefaultMobileInit()
|
|
{
|
|
m_StatCap = 225;
|
|
m_FollowersMax = 5;
|
|
Skills = new Skills(this);
|
|
Items = new List<Item>();
|
|
_statMods = new List<StatMod>();
|
|
_skillMods = new List<SkillMod>();
|
|
Map = Map.Internal;
|
|
AutoPageNotify = true;
|
|
Aggressors = new List<AggressorInfo>();
|
|
Aggressed = new List<AggressorInfo>();
|
|
DamageEntries = new List<DamageEntry>();
|
|
|
|
NextSkillTime = Core.TickCount;
|
|
}
|
|
|
|
public virtual void Delta(MobileDelta flag)
|
|
{
|
|
#if THREADGUARD
|
|
if (Thread.CurrentThread != Core.Thread)
|
|
{
|
|
Utility.PushColor(ConsoleColor.Red);
|
|
Console.WriteLine("Attempting to queue a delta change from an invalid thread!");
|
|
Console.WriteLine(new StackTrace());
|
|
Utility.PopColor();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
if (m_Map == null || m_Map == Map.Internal || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
m_DeltaFlags |= flag;
|
|
|
|
if (!m_InDeltaQueue)
|
|
{
|
|
m_InDeltaQueue = true;
|
|
m_DeltaQueue.Enqueue(this);
|
|
}
|
|
}
|
|
|
|
public static void ProcessDeltaQueue()
|
|
{
|
|
var limit = m_DeltaQueue.Count;
|
|
|
|
while (m_DeltaQueue.Count > 0 && --limit >= 0)
|
|
{
|
|
var mob = m_DeltaQueue.Dequeue();
|
|
|
|
if (mob == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
mob.m_InDeltaQueue = false;
|
|
|
|
try
|
|
{
|
|
mob.ProcessDelta();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
logger.Debug(ex, "Process Delta Queue for {Mobile} failed", mob);
|
|
}
|
|
}
|
|
|
|
if (m_DeltaQueue.Count > 0)
|
|
{
|
|
logger.Warning("{Count} mobiles left in delta queue after processing.", m_DeltaQueue.Count);
|
|
}
|
|
}
|
|
|
|
public virtual void OnKillsChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public bool CheckAlive(bool message = true)
|
|
{
|
|
if (Alive)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (message)
|
|
{
|
|
LocalOverheadMessage(MessageType.Regular, 0x3B2, 1019048); // I am dead and cannot do that.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void LaunchBrowser(ReadOnlySpan<char> url) => m_NetState?.LaunchBrowser(url);
|
|
|
|
public void InitStats(int str, int dex, int intel)
|
|
{
|
|
m_Str = str;
|
|
m_Dex = dex;
|
|
m_Int = intel;
|
|
|
|
Hits = HitsMax;
|
|
Stam = StamMax;
|
|
Mana = ManaMax;
|
|
|
|
Delta(MobileDelta.Stat | MobileDelta.Hits | MobileDelta.Stam | MobileDelta.Mana);
|
|
}
|
|
|
|
public virtual void DisplayPaperdollTo(Mobile to)
|
|
{
|
|
EventSink.InvokePaperdollRequest(to, this);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile requests to open his own paperdoll via the 'Open Paperdoll' macro.
|
|
/// </summary>
|
|
public virtual void OnPaperdollRequest()
|
|
{
|
|
if (CanPaperdollBeOpenedBy(this))
|
|
{
|
|
DisplayPaperdollTo(this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's stats.
|
|
/// </summary>
|
|
/// <param name="from"></param>
|
|
public virtual void OnStatsQuery(Mobile from)
|
|
{
|
|
if (from.Map == Map && Utility.InUpdateRange(Location, from.Location) && from.CanSee(this))
|
|
{
|
|
from.m_NetState.SendMobileStatus(from, this);
|
|
}
|
|
|
|
if (from == this)
|
|
{
|
|
m_NetState.SendStatLockInfo(this);
|
|
}
|
|
|
|
if (Party is IParty ip)
|
|
{
|
|
ip.OnStatsQuery(from, this);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when <paramref name="from" /> wants to see this Mobile's skills.
|
|
/// </summary>
|
|
public virtual void OnSkillsQuery(Mobile from)
|
|
{
|
|
if (from == this)
|
|
{
|
|
m_NetState.SendSkillsUpdate(Skills);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="Region" /> changes.
|
|
/// </summary>
|
|
public virtual void OnRegionChange(Region old, Region @new)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is single clicked.
|
|
/// </summary>
|
|
public virtual void OnSingleClick(Mobile from)
|
|
{
|
|
if (Deleted || AccessLevel == AccessLevel.Player && DisableHiddenSelfClick && Hidden && from == this)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (GuildClickMessage)
|
|
{
|
|
var guild = m_Guild;
|
|
|
|
if (guild != null && (m_DisplayGuildTitle || m_Player && guild.Type != GuildType.Regular))
|
|
{
|
|
var title = GuildTitle?.Trim() ?? "";
|
|
string type;
|
|
|
|
if (guild.Type >= 0 && (int)guild.Type < m_GuildTypes.Length)
|
|
{
|
|
type = m_GuildTypes[(int)guild.Type];
|
|
}
|
|
else
|
|
{
|
|
type = "";
|
|
}
|
|
|
|
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.
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the sector this Mobile is in gets <see cref="Map.Sector.Activate">activated</see>.
|
|
/// </summary>
|
|
public virtual void OnSectorActivate()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the sector this Mobile is in gets
|
|
/// <see cref="Map.Sector.Deactivate">deactivated</see>.
|
|
/// </summary>
|
|
public virtual void OnSectorDeactivate()
|
|
{
|
|
}
|
|
|
|
public static TimeSpan GetHitsRegenRate(Mobile m) => HitsRegenRateHandler?.Invoke(m) ?? DefaultHitsRate;
|
|
|
|
public static TimeSpan GetStamRegenRate(Mobile m) => StamRegenRateHandler?.Invoke(m) ?? DefaultStamRate;
|
|
|
|
public static TimeSpan GetManaRegenRate(Mobile m) => ManaRegenRateHandler?.Invoke(m) ?? DefaultManaRate;
|
|
|
|
public static 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<T>(PromptStateCallback<T> callback, PromptStateCallback<T> cancelCallback, T state) =>
|
|
Prompt = new SimpleStatePrompt<T>(callback, cancelCallback, state);
|
|
|
|
public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, bool callbackHandlesCancel, T state) =>
|
|
Prompt = new SimpleStatePrompt<T>(callback, callbackHandlesCancel, state);
|
|
|
|
public Prompt BeginPrompt<T>(PromptStateCallback<T> callback, T state) =>
|
|
BeginPrompt(callback, false, state);
|
|
|
|
public virtual int GetAngerSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetIdleSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 1;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetAttackSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 2;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetHurtSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 3;
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
public virtual int GetDeathSound()
|
|
{
|
|
if (BaseSoundID != 0)
|
|
{
|
|
return BaseSoundID + 4;
|
|
}
|
|
|
|
if (m_Body.IsHuman)
|
|
{
|
|
return Utility.Random(m_Female ? 0x314 : 0x423, m_Female ? 4 : 5);
|
|
}
|
|
|
|
return -1;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile <see cref="DoBeneficial">does a beneficial action</see>.
|
|
/// </summary>
|
|
public virtual void OnBeneficialAction(Mobile target, bool isCriminal)
|
|
{
|
|
if (isCriminal)
|
|
{
|
|
CriminalAction(false);
|
|
}
|
|
}
|
|
|
|
public virtual void DoBeneficial(Mobile target)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
OnBeneficialAction(target, IsBeneficialCriminal(target));
|
|
|
|
Region.OnBeneficialAction(this, target);
|
|
target.Region.OnGotBeneficialAction(this, target);
|
|
}
|
|
|
|
public virtual bool BeneficialCheck(Mobile target)
|
|
{
|
|
if (CanBeBeneficial(target, true))
|
|
{
|
|
DoBeneficial(target);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public virtual bool CanBeHarmful(Mobile target) => CanBeHarmful(target, true);
|
|
|
|
public virtual bool CanBeHarmful(Mobile target, bool message) => CanBeHarmful(target, message, false);
|
|
|
|
public virtual bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
|
|
{
|
|
if (target == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (Deleted || !ignoreOurBlessedness && m_Blessed || target.Deleted || target.m_Blessed || !Alive ||
|
|
IsDeadBondedPet || !target.Alive || target.IsDeadBondedPet)
|
|
{
|
|
if (message)
|
|
{
|
|
SendLocalizedMessage(1001018); // You can not perform negative acts on your target.
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (target == this)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
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;
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile <see cref="DoHarmful">does a harmful action</see>.
|
|
/// </summary>
|
|
public virtual void OnHarmfulAction(Mobile target, bool isCriminal)
|
|
{
|
|
if (isCriminal)
|
|
{
|
|
CriminalAction(false);
|
|
}
|
|
}
|
|
|
|
public virtual void DoHarmful(Mobile target, bool indirect = false)
|
|
{
|
|
if (target == null || Deleted)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var isCriminal = IsHarmfulCriminal(target);
|
|
|
|
OnHarmfulAction(target, isCriminal);
|
|
target.AggressiveAction(this, isCriminal);
|
|
|
|
Region.OnDidHarmful(this, target);
|
|
target.Region.OnGotHarmful(this, target);
|
|
|
|
if (!indirect && !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<StatMod>();
|
|
_statMods.Add(mod);
|
|
Delta(MobileDelta.Stat | GetStatDelta(mod.Type));
|
|
CheckStatTimers();
|
|
}
|
|
|
|
private static MobileDelta GetStatDelta(StatType type)
|
|
{
|
|
MobileDelta delta = 0;
|
|
|
|
if ((type & StatType.Str) != 0)
|
|
{
|
|
delta |= MobileDelta.Hits;
|
|
}
|
|
|
|
if ((type & StatType.Dex) != 0)
|
|
{
|
|
delta |= MobileDelta.Stam;
|
|
}
|
|
|
|
if ((type & StatType.Int) != 0)
|
|
{
|
|
delta |= MobileDelta.Mana;
|
|
}
|
|
|
|
return delta;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the total modified offset for the specified stat type.
|
|
/// </summary>
|
|
public int GetStatOffset(StatType type)
|
|
{
|
|
if (_statMods == null || _statMods.Count == 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var offset = 0;
|
|
|
|
for (var i = 0; i < _statMods.Count; i++)
|
|
{
|
|
var mod = _statMods[i];
|
|
if ((mod.Type & type) != 0 && !mod.HasElapsed())
|
|
{
|
|
offset += mod.Offset;
|
|
}
|
|
}
|
|
|
|
return offset;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawStr" /> changes.
|
|
/// <seealso cref="RawStr" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawStrChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when <see cref="RawDex" /> changes.
|
|
/// <seealso cref="RawDex" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawDexChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawInt" /> changes.
|
|
/// <seealso cref="RawInt" />
|
|
/// <seealso cref="OnRawStatChange" />
|
|
/// </summary>
|
|
public virtual void OnRawIntChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the <see cref="RawStr" />, <see cref="RawDex" />, or <see cref="RawInt" />
|
|
/// changes.
|
|
/// <seealso cref="OnRawStrChange" />
|
|
/// <seealso cref="OnRawDexChange" />
|
|
/// <seealso cref="OnRawIntChange" />
|
|
/// </summary>
|
|
public virtual void OnRawStatChange(StatType stat, int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnHitsChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnStamChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
public virtual void OnManaChange(int oldValue)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a call to <see cref="ApplyPoison" /> failed because <see cref="CheckPoisonImmunity" />
|
|
/// returned false: the Mobile was resistant to the poison. By default, this broadcasts an overhead message: * The poison
|
|
/// seems to have no effect. *
|
|
/// <seealso cref="CheckPoisonImmunity" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnPoisonImmunity(Mobile from, Poison poison)
|
|
{
|
|
PublicOverheadMessage(MessageType.Emote, 0x3B2, 1005534); // * The poison seems to have no effect. *
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="ApplyPoison" /> failed because
|
|
/// <see cref="CheckHigherPoison" /> returned false: the Mobile was already poisoned by an equal or greater strength poison.
|
|
/// <seealso cref="CheckHigherPoison" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnHigherPoison(Mobile from, Poison poison)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when a call to <see cref="ApplyPoison" /> succeeded. By default, this broadcasts an overhead
|
|
/// message varying by the level of the poison. Example: * Zippy begins to spasm uncontrollably. *
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnPoisoned(Mobile from, Poison poison, Poison oldPoison)
|
|
{
|
|
if (poison != null)
|
|
{
|
|
LocalOverheadMessage(MessageType.Regular, 0x21, 1042857 + poison.Level * 2);
|
|
NonlocalOverheadMessage(MessageType.Regular, 0x21, 1042858 + poison.Level * 2, Name);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is immune to some
|
|
/// <see cref="Poison" />. If true, <see cref="OnPoisonImmunity" /> will be invoked and
|
|
/// <see cref="ApplyPoisonResult.Immune" /> is returned.
|
|
/// <seealso cref="OnPoisonImmunity" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckPoisonImmunity(Mobile from, Poison poison) => false;
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="ApplyPoison" />, this method checks if the Mobile is already poisoned by some
|
|
/// <see cref="Poison" /> of equal or greater strength. If true, <see cref="OnHigherPoison" /> will be invoked and
|
|
/// <see cref="ApplyPoisonResult.HigherPoisonActive" /> is returned.
|
|
/// <seealso cref="OnHigherPoison" />
|
|
/// <seealso cref="ApplyPoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckHigherPoison(Mobile from, Poison poison) =>
|
|
m_Poison != null && m_Poison.Level >= poison.Level;
|
|
|
|
/// <summary>
|
|
/// Overridable. Attempts to apply poison to the Mobile. Checks are made such that no
|
|
/// <see cref="CheckHigherPoison">higher poison is active</see> and that the Mobile is not
|
|
/// <see cref="CheckPoisonImmunity">immune to the poison</see>. Provided those assertions are true, the
|
|
/// <paramref name="poison" /> is applied and <see cref="OnPoisoned" /> is invoked.
|
|
/// <seealso cref="Poison" />
|
|
/// <seealso cref="CurePoison" />
|
|
/// </summary>
|
|
/// <returns>
|
|
/// One of four possible values:
|
|
/// <list type="table">
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Cured">Cured</see>
|
|
/// </term>
|
|
/// <description>The <paramref name="poison" /> parameter was null and so <see cref="CurePoison" /> was invoked.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.HigherPoisonActive">HigherPoisonActive</see>
|
|
/// </term>
|
|
/// <description>The call to <see cref="CheckHigherPoison" /> returned false.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Immune">Immune</see>
|
|
/// </term>
|
|
/// <description>The call to <see cref="CheckPoisonImmunity" /> returned false.</description>
|
|
/// </item>
|
|
/// <item>
|
|
/// <term>
|
|
/// <see cref="ApplyPoisonResult.Poisoned">Poisoned</see>
|
|
/// </term>
|
|
/// <description>The <paramref name="poison" /> was successfully applied.</description>
|
|
/// </item>
|
|
/// </list>
|
|
/// </returns>
|
|
public virtual ApplyPoisonResult ApplyPoison(Mobile from, Poison poison)
|
|
{
|
|
if (poison == null)
|
|
{
|
|
CurePoison(from);
|
|
return ApplyPoisonResult.Cured;
|
|
}
|
|
|
|
if (CheckHigherPoison(from, poison))
|
|
{
|
|
OnHigherPoison(from, poison);
|
|
return ApplyPoisonResult.HigherPoisonActive;
|
|
}
|
|
|
|
if (CheckPoisonImmunity(from, poison))
|
|
{
|
|
OnPoisonImmunity(from, poison);
|
|
return ApplyPoisonResult.Immune;
|
|
}
|
|
|
|
var oldPoison = m_Poison;
|
|
Poison = poison;
|
|
|
|
OnPoisoned(from, poison, oldPoison);
|
|
|
|
return ApplyPoisonResult.Poisoned;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Called from <see cref="CurePoison" />, this method checks to see that the Mobile can be cured of
|
|
/// <see cref="Poison" />
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual bool CheckCure(Mobile from) => true;
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> succeeded.
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="CheckCure" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnCured(Mobile from, Poison oldPoison)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when a call to <see cref="CurePoison" /> failed.
|
|
/// <seealso cref="CurePoison" />
|
|
/// <seealso cref="CheckCure" />
|
|
/// <seealso cref="Poison" />
|
|
/// </summary>
|
|
public virtual void OnFailedCure(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Attempts to cure any poison that is currently active.
|
|
/// </summary>
|
|
/// <returns>True if poison was cured, false if otherwise.</returns>
|
|
public virtual bool CurePoison(Mobile from)
|
|
{
|
|
if (CheckCure(from))
|
|
{
|
|
var oldPoison = m_Poison;
|
|
Poison = null;
|
|
|
|
OnCured(from, oldPoison);
|
|
|
|
return true;
|
|
}
|
|
|
|
OnFailedCure(from);
|
|
|
|
return false;
|
|
}
|
|
|
|
public void MovingEffect(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode
|
|
) =>
|
|
Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes, hue, renderMode);
|
|
|
|
public void MovingEffect(IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes) =>
|
|
Effects.SendMovingEffect(this, to, itemID, speed, duration, fixedDirection, explodes);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode, int effect, int explodeEffect, int explodeSound, EffectLayer layer, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
hue,
|
|
renderMode,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
layer,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int hue, int renderMode, int effect, int explodeEffect, int explodeSound, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
hue,
|
|
renderMode,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
(EffectLayer)255,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int effect, int explodeEffect, int explodeSound, int unknown
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
unknown
|
|
);
|
|
|
|
public void MovingParticles(
|
|
IEntity to, int itemID, int speed, int duration, bool fixedDirection, bool explodes,
|
|
int effect, int explodeEffect, int explodeSound
|
|
) =>
|
|
Effects.SendMovingParticles(
|
|
this,
|
|
to,
|
|
itemID,
|
|
speed,
|
|
duration,
|
|
fixedDirection,
|
|
explodes,
|
|
0,
|
|
0,
|
|
effect,
|
|
explodeEffect,
|
|
explodeSound,
|
|
0
|
|
);
|
|
|
|
public void FixedEffect(int itemID, int speed, int duration, int hue, int renderMode) =>
|
|
Effects.SendTargetEffect(this, itemID, speed, duration, hue, renderMode);
|
|
|
|
public void FixedEffect(int itemID, int speed, int duration) =>
|
|
Effects.SendTargetEffect(this, itemID, speed, duration);
|
|
|
|
public void FixedParticles(
|
|
int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer, int unknown
|
|
) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer, unknown);
|
|
|
|
public void FixedParticles(
|
|
int itemID, int speed, int duration, int effect, int hue, int renderMode, EffectLayer layer
|
|
) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, hue, renderMode, effect, layer);
|
|
|
|
public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer, int unknown) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer, unknown);
|
|
|
|
public void FixedParticles(int itemID, int speed, int duration, int effect, EffectLayer layer) =>
|
|
Effects.SendTargetParticles(this, itemID, speed, duration, 0, 0, effect, layer);
|
|
|
|
public void BoltEffect(int hue) => Effects.SendBoltEffect(this, true, hue);
|
|
|
|
public Direction GetDirectionTo(int x, int y, bool run = false)
|
|
{
|
|
var dx = m_Location.m_X - x;
|
|
var dy = m_Location.m_Y - y;
|
|
|
|
var rx = (dx - dy) * 44;
|
|
var ry = (dx + dy) * 44;
|
|
|
|
var ax = rx.Abs();
|
|
var ay = ry.Abs();
|
|
|
|
Direction ret;
|
|
|
|
if ((ay >> 1) - ax >= 0)
|
|
{
|
|
ret = ry > 0 ? Direction.Up : Direction.Down;
|
|
}
|
|
else if ((ax >> 1) - ay >= 0)
|
|
{
|
|
ret = rx > 0 ? Direction.Left : Direction.Right;
|
|
}
|
|
else
|
|
{
|
|
ret = rx switch
|
|
{
|
|
>= 0 when ry >= 0 => Direction.West,
|
|
>= 0 => Direction.South,
|
|
< 0 when ry < 0 => Direction.East,
|
|
_ => Direction.North
|
|
};
|
|
}
|
|
|
|
if (run)
|
|
{
|
|
ret |= Direction.Running;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(Point2D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(Point3D p, bool run = false) => GetDirectionTo(p.m_X, p.m_Y, run);
|
|
|
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
|
public Direction GetDirectionTo(IPoint2D p, bool run = false) =>
|
|
p == null ? Direction.North | (run ? Direction.Running : 0) : GetDirectionTo(p.X, p.Y, run);
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is double clicked. By default, this method can either dismount or open the
|
|
/// paperdoll.
|
|
/// <seealso cref="CanPaperdollBeOpenedBy" />
|
|
/// <seealso cref="DisplayPaperdollTo" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClick(Mobile from)
|
|
{
|
|
if (this == from && (!DisableDismountInWarmode || !m_Warmode))
|
|
{
|
|
var mount = Mount;
|
|
|
|
if (mount != null)
|
|
{
|
|
mount.Rider = null;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (CanPaperdollBeOpenedBy(from))
|
|
{
|
|
DisplayPaperdollTo(from);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is double clicked by someone who is over 18 tiles away.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickOutOfRange(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Virtual event invoked when the Mobile is double clicked by someone who can no longer see the Mobile. This
|
|
/// may
|
|
/// happen, for example, using 'Last Object' after the Mobile has hidden.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickCantSee(Mobile from)
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Overridable. Event invoked when the Mobile is double clicked by someone who is not alive. Similar to
|
|
/// <see cref="OnDoubleClick" />, this method will show the paperdoll. It does not, however, provide any dismount
|
|
/// functionality.
|
|
/// <seealso cref="OnDoubleClick" />
|
|
/// </summary>
|
|
public virtual void OnDoubleClickDead(Mobile from)
|
|
{
|
|
if (CanPaperdollBeOpenedBy(from))
|
|
{
|
|
DisplayPaperdollTo(from);
|
|
}
|
|
}
|
|
|
|
private class ManaTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public ManaTimer(Mobile m) : base(GetManaRegenRate(m), GetManaRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenMana)
|
|
{
|
|
m_Owner.Mana++;
|
|
}
|
|
|
|
Delay = Interval = GetManaRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private class HitsTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public HitsTimer(Mobile m) : base(GetHitsRegenRate(m), GetHitsRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenHits)
|
|
{
|
|
m_Owner.Hits++;
|
|
}
|
|
|
|
Delay = Interval = GetHitsRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private class StamTimer : Timer
|
|
{
|
|
private readonly Mobile m_Owner;
|
|
|
|
public StamTimer(Mobile m) : base(GetStamRegenRate(m), GetStamRegenRate(m)) => m_Owner = m;
|
|
|
|
protected override void OnTick()
|
|
{
|
|
if (m_Owner.CanRegenStam)
|
|
{
|
|
m_Owner.Stam++;
|
|
}
|
|
|
|
Delay = Interval = GetStamRegenRate(m_Owner);
|
|
}
|
|
}
|
|
|
|
private void Logout()
|
|
{
|
|
if (m_Map != Map.Internal)
|
|
{
|
|
EventSink.InvokeLogout(this);
|
|
|
|
LogoutLocation = m_Location;
|
|
LogoutMap = m_Map;
|
|
|
|
Internalize();
|
|
}
|
|
}
|
|
|
|
private void ExpireCriminal()
|
|
{
|
|
Criminal = false;
|
|
}
|
|
}
|