diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 514157fc5..641413096 100644 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.Serialization; using Server.ContextMenus; using Server.Engines.PartySystem; using Server.Engines.Quests.Doom; @@ -9,445 +10,331 @@ using Server.Misc; using Server.Mobiles; using Server.Network; -namespace Server.Items +namespace Server.Items; + +public interface IDevourer { - public interface IDevourer + bool Devour(Corpse corpse); +} + +[Flags] +public enum CorpseFlag +{ + None = 0x00000000, + + /// + /// Has this corpse been carved? + /// + Carved = 0x00000001, + + /// + /// If true, this corpse will not turn into bones + /// + NoBones = 0x00000002, + + /// + /// If true, the corpse has turned into bones + /// + IsBones = 0x00000004, + + /// + /// Has this corpse yet been visited by a taxidermist? + /// + VisitedByTaxidermist = 0x00000008, + + /// + /// Has this corpse yet been used to channel spiritual energy? (AOS Spirit Speak) + /// + Channeled = 0x00000010, + + /// + /// Was the owner criminal when he died? + /// + Criminal = 0x00000020, + + /// + /// Has this corpse been animated? + /// + Animated = 0x00000040, + + /// + /// Has this corpse been self looted? + /// + SelfLooted = 0x00000080 +} + +[SerializationGenerator(13, false)] +public partial class Corpse : Container, ICarvable +{ + public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0); + + private static TimeSpan InstancedCorpseTime = TimeSpan.FromMinutes(3.0); + + private static TimeSpan _defaultDecayTime = TimeSpan.FromMinutes(7.0); + private static TimeSpan _boneDecayTime = TimeSpan.FromMinutes(7.0); + + private IDevourer _devourer; // The creature that devoured this corpse + + private Dictionary _instancedItems; + + [SerializableField(0)] + private List _restoreEquip; + + [SerializableField(1)] + private CorpseFlag _flags; + + [DeltaDateTime] + [SerializableField(2)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private DateTime _timeOfDeath; + + [SerializableField(3, getter: "private", setter: "private")] + private Dictionary _restoreTable; + + [TimerDrift] + [SerializableField(4, getter: "private", setter: "private")] + private Timer _decayTimer; + + [DeserializeTimerField(4)] + private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); + + [SerializableField(5, setter: "private")] + private HashSet _looters; + + [SerializableField(6, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Mobile _killer; + + [SerializableField(7, setter: "private")] + private List _aggressors; + + [SerializableField(8, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private Mobile _owner; + + // Value of the CorpseNameAttribute attached to the owner when he died -or- null if the owner had no CorpseNameAttribute; use "the remains of ~name~" + [SerializableField(9, getter: "private", setter: "private")] + private string _corpseName; + + [SerializableField(10, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private AccessLevel _accessLevel; + + [SerializableField(11, setter: "private")] + private Guild _guild; + + [SerializableField(12)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _kills; + + [SerializableField(13, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private List _equipItems; + + // Why was this public? + // public override bool IsPublicContainer => true; + + public Corpse(Mobile owner, List equipItems) : this(owner, null, null, equipItems) { - bool Devour(Corpse corpse); } - [Flags] - public enum CorpseFlag + public Corpse(Mobile owner, HairInfo hair, FacialHairInfo facialhair, List equipItems) + : base(0x2006) { - None = 0x00000000, + // To suppress console warnings, stackable must be true + Stackable = true; + Amount = owner.Body; // protocol defines that for itemid 0x2006, amount=body + Stackable = false; - /// - /// Has this corpse been carved? - /// - Carved = 0x00000001, + Movable = false; + Hue = owner.Hue; + Direction = owner.Direction; + Name = owner.Name; - /// - /// If true, this corpse will not turn into bones - /// - NoBones = 0x00000002, + _owner = owner; - /// - /// If true, the corpse has turned into bones - /// - IsBones = 0x00000004, + _corpseName = GetCorpseName(owner); - /// - /// Has this corpse yet been visited by a taxidermist? - /// - VisitedByTaxidermist = 0x00000008, + _timeOfDeath = Core.Now; - /// - /// Has this corpse yet been used to channel spiritual energy? (AOS Spirit Speak) - /// - Channeled = 0x00000010, + _accessLevel = owner.AccessLevel; + _guild = owner.Guild as Guild; + _kills = owner.Kills; + SetFlag(CorpseFlag.Criminal, owner.Criminal); - /// - /// Was the owner criminal when he died? - /// - Criminal = 0x00000020, + Hair = hair; + FacialHair = facialhair; - /// - /// Has this corpse been animated? - /// - Animated = 0x00000040, + // This corpse does not turn to bones if: the owner is not a player + SetFlag(CorpseFlag.NoBones, !owner.Player); - /// - /// Has this corpse been self looted? - /// - SelfLooted = 0x00000080 + _looters = new HashSet(); + _equipItems = equipItems; + + _aggressors = new List(owner.Aggressors.Count + owner.Aggressed.Count); + + var isBaseCreature = owner is BaseCreature; + + var lastTime = TimeSpan.MaxValue; + + for (var i = 0; i < owner.Aggressors.Count; ++i) + { + var info = owner.Aggressors[i]; + + if (Core.Now - info.LastCombatTime < lastTime) + { + _killer = info.Attacker; + lastTime = Core.Now - info.LastCombatTime; + } + + if (!isBaseCreature && !info.CriminalAggression) + { + _aggressors.Add(info.Attacker); + } + } + + for (var i = 0; i < owner.Aggressed.Count; ++i) + { + var info = owner.Aggressed[i]; + + if (Core.Now - info.LastCombatTime < lastTime) + { + _killer = info.Defender; + lastTime = Core.Now - info.LastCombatTime; + } + + if (!isBaseCreature) + { + _aggressors.Add(info.Defender); + } + } + + if (isBaseCreature) + { + var bc = (BaseCreature)owner; + + var master = bc.GetMaster(); + if (master != null) + { + _aggressors.Add(master); + } + + var rights = BaseCreature.GetLootingRights(bc.DamageEntries, bc.HitsMax); + for (var i = 0; i < rights.Count; ++i) + { + var ds = rights[i]; + + if (ds.m_HasRight) + { + _aggressors.Add(ds.m_Mobile); + } + } + } + + BeginDecay(_defaultDecayTime); + + DevourCorpse(); } - public class Corpse : Container, ICarvable + [CommandProperty(AccessLevel.GameMaster)] + public virtual bool InstancedCorpse => Core.SE && Core.Now < TimeOfDeath + InstancedCorpseTime; + + public override bool IsDecoContainer => false; + + public override bool DisplayWeight => false; + + // Name of the first PlayerMobile who used Forensic Evaluation on the corpse + [CommandProperty(AccessLevel.GameMaster)] + public string Forensicist { get; set; } + + public HairInfo Hair { get; } + + public FacialHairInfo FacialHair { get; } + + [CommandProperty(AccessLevel.GameMaster)] + public bool IsBones => GetFlag(CorpseFlag.IsBones); + + [CommandProperty(AccessLevel.GameMaster)] + public bool Devoured => _devourer != null; + + [CommandProperty(AccessLevel.GameMaster)] + public bool Carved { - public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0); + get => GetFlag(CorpseFlag.Carved); + set => SetFlag(CorpseFlag.Carved, value); + } - public static readonly TimeSpan InstancedCorpseTime = TimeSpan.FromMinutes(3.0); + [CommandProperty(AccessLevel.GameMaster)] + public bool VisitedByTaxidermist + { + get => GetFlag(CorpseFlag.VisitedByTaxidermist); + set => SetFlag(CorpseFlag.VisitedByTaxidermist, value); + } - private static readonly TimeSpan m_DefaultDecayTime = TimeSpan.FromMinutes(7.0); - private static readonly TimeSpan m_BoneDecayTime = TimeSpan.FromMinutes(7.0); + [CommandProperty(AccessLevel.GameMaster)] + public bool Channeled + { + get => GetFlag(CorpseFlag.Channeled); + set => SetFlag(CorpseFlag.Channeled, value); + } - private string - m_CorpseName; // Value of the CorpseNameAttribute attached to the owner when he died -or- null if the owner had no CorpseNameAttribute; use "the remains of ~name~" + [CommandProperty(AccessLevel.GameMaster)] + public bool Animated + { + get => GetFlag(CorpseFlag.Animated); + set => SetFlag(CorpseFlag.Animated, value); + } - private DateTime m_DecayTime; + [CommandProperty(AccessLevel.GameMaster)] + public bool SelfLooted + { + get => GetFlag(CorpseFlag.SelfLooted); + set => SetFlag(CorpseFlag.SelfLooted, value); + } - private Timer m_DecayTimer; - private IDevourer m_Devourer; // The creature that devoured this corpse - private CorpseFlag m_Flags; // @see CorpseFlag + [CommandProperty(AccessLevel.GameMaster)] + public bool Criminal + { + get => GetFlag(CorpseFlag.Criminal); + set => SetFlag(CorpseFlag.Criminal, value); + } - // For notoriety: + public override bool DisplaysContent => false; - // For Forensics Evaluation - public string m_Forensicist; // Name of the first PlayerMobile who used Forensic Evaluation on the corpse - - private Dictionary m_InstancedItems; - - private Dictionary m_RestoreTable; - - // Why was this public? - // public override bool IsPublicContainer => true; - - public Corpse(Mobile owner, List equipItems) : this(owner, null, null, equipItems) + public void Carve(Mobile from, Item item) + { + if (IsCriminalAction(from) && (Map?.Rules & MapRules.HarmfulRestrictions) != 0) { - } - - public Corpse(Mobile owner, HairInfo hair, FacialHairInfo facialhair, List equipItems) - : base(0x2006) - { - // To suppress console warnings, stackable must be true - Stackable = true; - Amount = owner.Body; // protocol defines that for itemid 0x2006, amount=body - Stackable = false; - - Movable = false; - Hue = owner.Hue; - Direction = owner.Direction; - Name = owner.Name; - - Owner = owner; - - m_CorpseName = GetCorpseName(owner); - - TimeOfDeath = Core.Now; - - AccessLevel = owner.AccessLevel; - Guild = owner.Guild as Guild; - Kills = owner.Kills; - SetFlag(CorpseFlag.Criminal, owner.Criminal); - - Hair = hair; - FacialHair = facialhair; - - // This corpse does not turn to bones if: the owner is not a player - SetFlag(CorpseFlag.NoBones, !owner.Player); - - Looters = new List(); - EquipItems = equipItems; - - Aggressors = new List(owner.Aggressors.Count + owner.Aggressed.Count); - // bool addToAggressors = !( owner is BaseCreature ); - - var isBaseCreature = owner is BaseCreature; - - var lastTime = TimeSpan.MaxValue; - - for (var i = 0; i < owner.Aggressors.Count; ++i) + if (Owner?.Player != true) { - var info = owner.Aggressors[i]; - - if (Core.Now - info.LastCombatTime < lastTime) - { - Killer = info.Attacker; - lastTime = Core.Now - info.LastCombatTime; - } - - if (!isBaseCreature && !info.CriminalAggression) - { - Aggressors.Add(info.Attacker); - } - } - - for (var i = 0; i < owner.Aggressed.Count; ++i) - { - var info = owner.Aggressed[i]; - - if (Core.Now - info.LastCombatTime < lastTime) - { - Killer = info.Defender; - lastTime = Core.Now - info.LastCombatTime; - } - - if (!isBaseCreature) - { - Aggressors.Add(info.Defender); - } - } - - if (isBaseCreature) - { - var bc = (BaseCreature)owner; - - var master = bc.GetMaster(); - if (master != null) - { - Aggressors.Add(master); - } - - var rights = BaseCreature.GetLootingRights(bc.DamageEntries, bc.HitsMax); - for (var i = 0; i < rights.Count; ++i) - { - var ds = rights[i]; - - if (ds.m_HasRight) - { - Aggressors.Add(ds.m_Mobile); - } - } - } - - BeginDecay(m_DefaultDecayTime); - - DevourCorpse(); - } - - public Corpse(Serial serial) : base(serial) - { - } - - [CommandProperty(AccessLevel.GameMaster)] - public virtual bool InstancedCorpse => Core.SE && Core.Now < TimeOfDeath + InstancedCorpseTime; - - public override bool IsDecoContainer => false; - - [CommandProperty(AccessLevel.GameMaster)] - public DateTime TimeOfDeath { get; set; } - - public override bool DisplayWeight => false; - - public HairInfo Hair { get; } - - public FacialHairInfo FacialHair { get; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool IsBones => GetFlag(CorpseFlag.IsBones); - - [CommandProperty(AccessLevel.GameMaster)] - public bool Devoured => m_Devourer != null; - - [CommandProperty(AccessLevel.GameMaster)] - public bool Carved - { - get => GetFlag(CorpseFlag.Carved); - set => SetFlag(CorpseFlag.Carved, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool VisitedByTaxidermist - { - get => GetFlag(CorpseFlag.VisitedByTaxidermist); - set => SetFlag(CorpseFlag.VisitedByTaxidermist, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool Channeled - { - get => GetFlag(CorpseFlag.Channeled); - set => SetFlag(CorpseFlag.Channeled, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool Animated - { - get => GetFlag(CorpseFlag.Animated); - set => SetFlag(CorpseFlag.Animated, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public bool SelfLooted - { - get => GetFlag(CorpseFlag.SelfLooted); - set => SetFlag(CorpseFlag.SelfLooted, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public AccessLevel AccessLevel { get; private set; } - - public List Aggressors { get; private set; } - - public List Looters { get; private set; } - - [CommandProperty(AccessLevel.GameMaster)] - public Mobile Killer { get; private set; } - - public List EquipItems { get; private set; } - - public List RestoreEquip { get; set; } - - public Guild Guild { get; } - - [CommandProperty(AccessLevel.GameMaster)] - public int Kills { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public bool Criminal - { - get => GetFlag(CorpseFlag.Criminal); - set => SetFlag(CorpseFlag.Criminal, value); - } - - [CommandProperty(AccessLevel.GameMaster)] - public Mobile Owner { get; private set; } - - public override bool DisplaysContent => false; - - public void Carve(Mobile from, Item item) - { - if (IsCriminalAction(from) && (Map?.Rules & MapRules.HarmfulRestrictions) != 0) - { - if (Owner?.Player != true) - { - from.SendLocalizedMessage(1005035); // You did not earn the right to loot this creature! - } - else - { - from.SendLocalizedMessage(1010049); // You may not loot this corpse. - } - - return; - } - - var dead = Owner; - - if (GetFlag(CorpseFlag.Carved) || dead == null) - { - from.SendLocalizedMessage(500485); // You see nothing useful to carve from the corpse. - } - else if (((Body)Amount).IsHuman && ItemID == 0x2006) - { - new Blood(0x122D).MoveToWorld(Location, Map); - - new Torso().MoveToWorld(Location, Map); - new LeftLeg().MoveToWorld(Location, Map); - new LeftArm().MoveToWorld(Location, Map); - new RightLeg().MoveToWorld(Location, Map); - new RightArm().MoveToWorld(Location, Map); - new Head(dead.Name).MoveToWorld(Location, Map); - - SetFlag(CorpseFlag.Carved, true); - - ProcessDelta(); - SendRemovePacket(); - ItemID = Utility.Random(0xECA, 9); // bone graphic - Hue = 0; - ProcessDelta(); - - if (IsCriminalAction(from)) - { - from.CriminalAction(true); - } - } - else if (dead is BaseCreature creature) - { - creature.OnCarve(from, this, item); + from.SendLocalizedMessage(1005035); // You did not earn the right to loot this creature! } else { - from.SendLocalizedMessage(500485); // You see nothing useful to carve from the corpse. + from.SendLocalizedMessage(1010049); // You may not loot this corpse. } + + return; } - public override bool IsChildVisibleTo(Mobile m, Item child) => - !m.Player || m.AccessLevel > AccessLevel.Player || m_InstancedItems == null || - !m_InstancedItems.TryGetValue(child, out var info) || !InstancedCorpse && !info.Perpetual - || info.IsOwner(m); + var dead = Owner; - private void AssignInstancedLoot() + if (GetFlag(CorpseFlag.Carved) || dead == null) { - if (Aggressors.Count == 0 || Items.Count == 0) - { - return; - } - - m_InstancedItems ??= new Dictionary(); - - var m_Stackables = new List(); - var m_Unstackables = new List(); - - for (var i = 0; i < Items.Count; i++) - { - var item = Items[i]; - - if (item.LootType != LootType.Cursed) // Don't have cursed items take up someone's item spot.. (?) - { - if (item.Stackable) - { - m_Stackables.Add(item); - } - else - { - m_Unstackables.Add(item); - } - } - } - - var attackers = new List(Aggressors); - - for (var i = 1; i < attackers.Count - 1; i++) // randomize - { - var rand = Utility.Random(i + 1); - - (attackers[rand], attackers[i]) = (attackers[i], attackers[rand]); - } - - // stackables first, for the remaining stackables, have those be randomly added after - - for (var i = 0; i < m_Stackables.Count; i++) - { - var item = m_Stackables[i]; - - if (item.Amount >= attackers.Count) - { - var amountPerAttacker = Math.DivRem(item.Amount, attackers.Count, out var remainder); - - for (var j = 0; j < (remainder == 0 ? attackers.Count - 1 : attackers.Count); j++) - { - // LiftItemDupe automagically adds it as a child item to the corpse - var splitItem = - Mobile.LiftItemDupe(item, item.Amount - amountPerAttacker); - - m_InstancedItems.Add(splitItem, new InstancedItemInfo(splitItem, attackers[j])); - - // What happens to the remaining portion? TEMP FOR NOW UNTIL OSI VERIFICATION: Treat as Single Item. - } - - if (remainder == 0) - { - m_InstancedItems.Add(item, new InstancedItemInfo(item, attackers[^1])); - } - else - { - m_Unstackables.Add(item); - } - } - else - { - // What happens in this case? TEMP FOR NOW UNTIL OSI VERIFICATION: Treat as Single Item. - m_Unstackables.Add(item); - } - } - - for (var i = 0; i < m_Unstackables.Count; i++) - { - var m = attackers[i % attackers.Count]; - var item = m_Unstackables[i]; - - m_InstancedItems.Add(item, new InstancedItemInfo(item, m)); - } + from.SendLocalizedMessage(500485); // You see nothing useful to carve from the corpse. } - - public void AddCarvedItem(Item carved, Mobile carver) + else if (((Body)Amount).IsHuman && ItemID == 0x2006) { - DropItem(carved); + new Blood(0x122D).MoveToWorld(Location, Map); - if (InstancedCorpse) - { - m_InstancedItems ??= new Dictionary(); + new Torso().MoveToWorld(Location, Map); + new LeftLeg().MoveToWorld(Location, Map); + new LeftArm().MoveToWorld(Location, Map); + new RightLeg().MoveToWorld(Location, Map); + new RightArm().MoveToWorld(Location, Map); + new Head(dead.Name).MoveToWorld(Location, Map); - m_InstancedItems.Add(carved, new InstancedItemInfo(carved, carver)); - } - } - - public void TurnToBones() - { - if (Deleted) - { - return; - } + SetFlag(CorpseFlag.Carved, true); ProcessDelta(); SendRemovePacket(); @@ -455,793 +342,723 @@ namespace Server.Items Hue = 0; ProcessDelta(); - SetFlag(CorpseFlag.NoBones, true); - SetFlag(CorpseFlag.IsBones, true); - - BeginDecay(m_BoneDecayTime); - } - - public void BeginDecay(TimeSpan delay) - { - m_DecayTimer?.Stop(); - - m_DecayTime = Core.Now + delay; - - m_DecayTimer = new InternalTimer(this, delay); - m_DecayTimer.Start(); - } - - public override void OnAfterDelete() - { - m_DecayTimer?.Stop(); - - m_DecayTimer = null; - } - - public static string GetCorpseName(Mobile m) => m is BaseCreature bc ? bc.CorpseNameOverride ?? bc.CorpseName : null; - - public static void Initialize() - { - Mobile.CreateCorpseHandler += Mobile_CreateCorpseHandler; - } - - public static Container Mobile_CreateCorpseHandler( - Mobile owner, HairInfo hair, FacialHairInfo facialhair, - List initialContent, List equipItems - ) - { - var c = owner is MilitiaFighter - ? new MilitiaFighterCorpse(owner, hair, facialhair, equipItems) - : new Corpse(owner, hair, facialhair, equipItems); - - owner.Corpse = c; - - for (var i = 0; i < initialContent.Count; ++i) + if (IsCriminalAction(from)) { - var item = initialContent[i]; + from.CriminalAction(true); + } + } + else if (dead is BaseCreature creature) + { + creature.OnCarve(from, this, item); + } + else + { + from.SendLocalizedMessage(500485); // You see nothing useful to carve from the corpse. + } + } - if (Core.AOS && owner.Player && item.Parent == owner.Backpack) + public override bool IsChildVisibleTo(Mobile m, Item child) => + !m.Player || m.AccessLevel > AccessLevel.Player || _instancedItems == null || + !_instancedItems.TryGetValue(child, out var info) || !InstancedCorpse && !info.Perpetual + || info.IsOwner(m); + + private void AssignInstancedLoot() + { + if (_aggressors.Count == 0 || Items.Count == 0) + { + return; + } + + _instancedItems ??= new Dictionary(); + + var m_Stackables = new List(); + var m_Unstackables = new List(); + + for (var i = 0; i < Items.Count; i++) + { + var item = Items[i]; + + if (item.LootType != LootType.Cursed) // Don't have cursed items take up someone's item spot.. (?) + { + if (item.Stackable) { - c.AddItem(item); + m_Stackables.Add(item); } else { - c.DropItem(item); + m_Unstackables.Add(item); } + } + } - if (owner.Player && Core.AOS) + var attackers = new List(_aggressors); + + for (var i = 1; i < attackers.Count - 1; i++) // randomize + { + var rand = Utility.Random(i + 1); + + (attackers[rand], attackers[i]) = (attackers[i], attackers[rand]); + } + + // stackables first, for the remaining stackables, have those be randomly added after + + for (var i = 0; i < m_Stackables.Count; i++) + { + var item = m_Stackables[i]; + + if (item.Amount >= attackers.Count) + { + var amountPerAttacker = Math.DivRem(item.Amount, attackers.Count, out var remainder); + + for (var j = 0; j < (remainder == 0 ? attackers.Count - 1 : attackers.Count); j++) { - c.SetRestoreInfo(item, item.Location); + // LiftItemDupe automagically adds it as a child item to the corpse + var splitItem = Mobile.LiftItemDupe(item, item.Amount - amountPerAttacker); + + _instancedItems.Add(splitItem, new InstancedItemInfo(splitItem, attackers[j])); + + // What happens to the remaining portion? TEMP FOR NOW UNTIL OSI VERIFICATION: Treat as Single Item. } - } - if (Core.SE && !owner.Player) - { - c.AssignInstancedLoot(); - } - else if (Core.AOS && owner is PlayerMobile pm) - { - c.RestoreEquip = pm.EquipSnapshot; - } - - var loc = owner.Location; - var map = owner.Map; - - if (map == null || map == Map.Internal) - { - loc = owner.LogoutLocation; - map = owner.LogoutMap; - } - - c.MoveToWorld(loc, map); - - return c; - } - - protected bool GetFlag(CorpseFlag flag) => (m_Flags & flag) != 0; - - protected void SetFlag(CorpseFlag flag, bool on) - { - m_Flags = on ? m_Flags | flag : m_Flags & ~flag; - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(12); // version - - if (RestoreEquip == null) - { - writer.Write(false); + if (remainder == 0) + { + _instancedItems.Add(item, new InstancedItemInfo(item, attackers[^1])); + } + else + { + m_Unstackables.Add(item); + } } else { - writer.Write(true); - writer.Write(RestoreEquip); - } - - writer.Write((int)m_Flags); - - writer.WriteDeltaTime(TimeOfDeath); - - var count = m_RestoreTable?.Count ?? 0; - writer.Write(count); - - if (m_RestoreTable != null) - { - foreach (var (item, loc) in m_RestoreTable) - { - writer.Write(item); - - if (item.Location == loc) - { - writer.Write(false); - } - else - { - writer.Write(true); - writer.Write(loc); - } - } - } - - writer.Write(m_DecayTimer != null); - - if (m_DecayTimer != null) - { - writer.WriteDeltaTime(m_DecayTime); - } - - writer.Write(Looters); - writer.Write(Killer); - - writer.Write(Aggressors); - - writer.Write(Owner); - - writer.Write(m_CorpseName); - - writer.Write((int)AccessLevel); - writer.Write(Guild); - writer.Write(Kills); - - writer.Write(EquipItems); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 12: - { - if (reader.ReadBool()) - { - RestoreEquip = reader.ReadEntityList(); - } - - goto case 11; - } - case 11: - { - // Version 11, we move all bools to a CorpseFlag - m_Flags = (CorpseFlag)reader.ReadInt(); - - TimeOfDeath = reader.ReadDeltaTime(); - - var count = reader.ReadInt(); - - for (var i = 0; i < count; ++i) - { - var item = reader.ReadEntity(); - - if (reader.ReadBool()) - { - SetRestoreInfo(item, reader.ReadPoint3D()); - } - else if (item != null) - { - SetRestoreInfo(item, item.Location); - } - } - - if (reader.ReadBool()) - { - BeginDecay(reader.ReadDeltaTime() - Core.Now); - } - - Looters = reader.ReadEntityList(); - Killer = reader.ReadEntity(); - - Aggressors = reader.ReadEntityList(); - Owner = reader.ReadEntity(); - - m_CorpseName = reader.ReadString(); - - AccessLevel = (AccessLevel)reader.ReadInt(); - reader.ReadInt(); // guild reserve - Kills = reader.ReadInt(); - - EquipItems = reader.ReadEntityList(); - break; - } - case 10: - { - TimeOfDeath = reader.ReadDeltaTime(); - - goto case 9; - } - case 9: - { - var count = reader.ReadInt(); - - for (var i = 0; i < count; ++i) - { - var item = reader.ReadEntity(); - - if (reader.ReadBool()) - { - SetRestoreInfo(item, reader.ReadPoint3D()); - } - else if (item != null) - { - SetRestoreInfo(item, item.Location); - } - } - - goto case 8; - } - case 8: - { - SetFlag(CorpseFlag.VisitedByTaxidermist, reader.ReadBool()); - - goto case 7; - } - case 7: - { - if (reader.ReadBool()) - { - BeginDecay(reader.ReadDeltaTime() - Core.Now); - } - - goto case 6; - } - case 6: - { - Looters = reader.ReadEntityList(); - Killer = reader.ReadEntity(); - - goto case 5; - } - case 5: - { - SetFlag(CorpseFlag.Carved, reader.ReadBool()); - - goto case 4; - } - case 4: - { - Aggressors = reader.ReadEntityList(); - - goto case 3; - } - case 3: - { - Owner = reader.ReadEntity(); - - goto case 2; - } - case 2: - { - SetFlag(CorpseFlag.NoBones, reader.ReadBool()); - - goto case 1; - } - case 1: - { - m_CorpseName = reader.ReadString(); - - goto case 0; - } - case 0: - { - if (version < 10) - { - TimeOfDeath = Core.Now; - } - - if (version < 7) - { - BeginDecay(m_DefaultDecayTime); - } - - if (version < 6) - { - Looters = new List(); - } - - if (version < 4) - { - Aggressors = new List(); - } - - AccessLevel = (AccessLevel)reader.ReadInt(); - reader.ReadInt(); // guild reserve - Kills = reader.ReadInt(); - SetFlag(CorpseFlag.Criminal, reader.ReadBool()); - - EquipItems = reader.ReadEntityList(); - - break; - } + // What happens in this case? TEMP FOR NOW UNTIL OSI VERIFICATION: Treat as Single Item. + m_Unstackables.Add(item); } } - public bool DevourCorpse() + for (var i = 0; i < m_Unstackables.Count; i++) { - if (Devoured || Deleted || Killer?.Deleted != false || !Killer.Alive || Killer is not IDevourer devourer || - Owner?.Deleted != false) + var m = attackers[i % attackers.Count]; + var item = m_Unstackables[i]; + + _instancedItems.Add(item, new InstancedItemInfo(item, m)); + } + } + + public void AddCarvedItem(Item carved, Mobile carver) + { + DropItem(carved); + + if (InstancedCorpse) + { + _instancedItems ??= new Dictionary(); + + _instancedItems.Add(carved, new InstancedItemInfo(carved, carver)); + } + } + + public void TurnToBones() + { + if (Deleted) + { + return; + } + + ProcessDelta(); + SendRemovePacket(); + ItemID = Utility.Random(0xECA, 9); // bone graphic + Hue = 0; + ProcessDelta(); + + SetFlag(CorpseFlag.NoBones, true); + SetFlag(CorpseFlag.IsBones, true); + + BeginDecay(_boneDecayTime); + } + + public void BeginDecay(TimeSpan delay) + { + _decayTimer?.Stop(); + _decayTimer = new InternalTimer(this, delay); + _decayTimer.Start(); + } + + public override void OnAfterDelete() + { + _decayTimer?.Stop(); + _decayTimer = null; + } + + public static string GetCorpseName(Mobile m) => m is BaseCreature bc ? bc.CorpseNameOverride ?? bc.CorpseName : null; + + public static void Initialize() + { + Mobile.CreateCorpseHandler += Mobile_CreateCorpseHandler; + } + + public static Container Mobile_CreateCorpseHandler( + Mobile owner, HairInfo hair, FacialHairInfo facialhair, + List initialContent, List equipItems + ) + { + var c = owner is MilitiaFighter + ? new MilitiaFighterCorpse(owner, hair, facialhair, equipItems) + : new Corpse(owner, hair, facialhair, equipItems); + + owner.Corpse = c; + + for (var i = 0; i < initialContent.Count; ++i) + { + var item = initialContent[i]; + + if (Core.AOS && owner.Player && item.Parent == owner.Backpack) + { + c.AddItem(item); + } + else + { + c.DropItem(item); + } + + if (owner.Player && Core.AOS) + { + c.SetRestoreInfo(item, item.Location); + } + } + + if (Core.SE && !owner.Player) + { + c.AssignInstancedLoot(); + } + else if (Core.AOS && owner is PlayerMobile pm) + { + c.RestoreEquip = pm.EquipSnapshot; + } + + var loc = owner.Location; + var map = owner.Map; + + if (map == null || map == Map.Internal) + { + loc = owner.LogoutLocation; + map = owner.LogoutMap; + } + + c.MoveToWorld(loc, map); + + return c; + } + + protected bool GetFlag(CorpseFlag flag) => (_flags & flag) != 0; + + protected void SetFlag(CorpseFlag flag, bool value) + { + if (value) + { + Flags |= flag; + } + else + { + Flags &= ~flag; + } + } + + private void Deserialize(IGenericReader reader, int version) + { + if (reader.ReadBool()) + { + _restoreEquip = reader.ReadEntityList(); + } + + _flags = (CorpseFlag)reader.ReadInt(); + + _timeOfDeath = reader.ReadDeltaTime(); + + var count = reader.ReadInt(); + + for (var i = 0; i < count; ++i) + { + var item = reader.ReadEntity(); + + if (reader.ReadBool()) + { + SetRestoreInfo(item, reader.ReadPoint3D()); + } + else if (item != null) + { + SetRestoreInfo(item, item.Location); + } + } + + if (reader.ReadBool()) + { + BeginDecay(reader.ReadDeltaTime() - Core.Now); + } + + _looters = reader.ReadEntitySet(); + _killer = reader.ReadEntity(); + + _aggressors = reader.ReadEntityList(); + _owner = reader.ReadEntity(); + + _corpseName = reader.ReadString(); + + _accessLevel = (AccessLevel)reader.ReadInt(); + reader.ReadInt(); // guild reserve + _kills = reader.ReadInt(); + + _equipItems = reader.ReadEntityList(); + } + + public bool DevourCorpse() + { + if (Devoured || Deleted || Killer?.Deleted != false || !Killer.Alive || Killer is not IDevourer devourer || + Owner?.Deleted != false) + { + return false; + } + + _devourer = devourer; // Set the devourer the killer + return _devourer.Devour(this); // Devour the corpse if it hasn't + } + + public override void SendInfoTo(NetState ns, ReadOnlySpan world = default) + { + base.SendInfoTo(ns, world); + + if (((Body)Amount).IsHuman && ItemID == 0x2006) + { + ns.SendCorpseContent(ns.Mobile, this); + ns.SendCorpseEquip(ns.Mobile, this); + } + } + + public bool IsCriminalAction(Mobile from) + { + if (from == Owner || from.AccessLevel >= AccessLevel.GameMaster) + { + return false; + } + + var p = Party.Get(Owner); + + if (p?.Contains(from) == true) + { + var pmi = p[Owner]; + + if (pmi?.CanLoot == true) { return false; } - - m_Devourer = devourer; // Set the devourer the killer - return m_Devourer.Devour(this); // Devour the corpse if it hasn't } - public override void SendInfoTo(NetState ns, ReadOnlySpan world = default) - { - base.SendInfoTo(ns, world); + return NotorietyHandlers.CorpseNotoriety(from, this) == Notoriety.Innocent; + } - if (((Body)Amount).IsHuman && ItemID == 0x2006) + public override bool CheckItemUse(Mobile from, Item item) => + base.CheckItemUse(from, item) && (item == this || CanLoot(from, item)); + + public override bool CheckLift(Mobile from, Item item, ref LRReason reject) => + base.CheckLift(from, item, ref reject) && CanLoot(from, item); + + public override void OnItemUsed(Mobile from, Item item) + { + base.OnItemUsed(from, item); + + if (item is Food) + { + from.RevealingAction(); + } + + if (item != this && IsCriminalAction(from)) + { + from.CriminalAction(true); + } + + this.Add(_looters, from); + + _instancedItems?.Remove(item); + + } + + public override void OnItemLifted(Mobile from, Item item) + { + base.OnItemLifted(from, item); + + if (item != this && from != Owner) + { + from.RevealingAction(); + } + + if (item != this && IsCriminalAction(from)) + { + from.CriminalAction(true); + } + + this.Add(_looters, from); + + _instancedItems?.Remove(item); + } + + public override void GetContextMenuEntries(Mobile from, List list) + { + base.GetContextMenuEntries(from, list); + + if (Core.AOS && _owner == from && from.Alive) + { + list.Add(new OpenCorpseEntry()); + } + } + + public bool GetRestoreInfo(Item item, ref Point3D loc) => + item != null && _restoreTable?.TryGetValue(item, out loc) == true; + + public void SetRestoreInfo(Item item, Point3D loc) + { + if (item == null) + { + return; + } + + _restoreTable ??= new Dictionary(); + _restoreTable[item] = loc; + this.MarkDirty(); + } + + public void ClearRestoreInfo(Item item) + { + if (_restoreTable == null || item == null) + { + return; + } + + _restoreTable.Remove(item); + if (_restoreTable.Count == 0) + { + RestoreTable = null; + } + else + { + this.MarkDirty(); + } + } + + public bool CanLoot(Mobile from, Item item) => + !IsCriminalAction(from) || (Map.Rules & MapRules.HarmfulRestrictions) == 0; + + public bool CheckLoot(Mobile from, Item item) + { + if (!CanLoot(from, item)) + { + if (Owner?.Player != true) { - ns.SendCorpseContent(ns.Mobile, this); - ns.SendCorpseEquip(ns.Mobile, this); + from.SendLocalizedMessage(1005035); // You did not earn the right to loot this creature! + } + else + { + from.SendLocalizedMessage(1010049); // You may not loot this corpse. + } + + return false; + } + + if (IsCriminalAction(from)) + { + if (Owner?.Player != true) + { + from.SendLocalizedMessage(1005036); // Looting this monster corpse will be a criminal act! + } + else + { + from.SendLocalizedMessage(1005038); // Looting this corpse will be a criminal act! } } - public bool IsCriminalAction(Mobile from) + return true; + } + + public virtual void Open(Mobile from, bool checkSelfLoot) + { + if (from.AccessLevel <= AccessLevel.Player && !from.InRange(GetWorldLocation(), 2)) { - if (from == Owner || from.AccessLevel >= AccessLevel.GameMaster) + from.SendLocalizedMessage(500446); // That is too far away. + return; + } + + if (checkSelfLoot && from == Owner && !GetFlag(CorpseFlag.SelfLooted) && Items.Count != 0) + { + if (from.FindItemOnLayer(Layer.OuterTorso) is DeathRobe robe) { - return false; - } + var map = from.Map; - var p = Party.Get(Owner); - - if (p?.Contains(from) == true) - { - var pmi = p[Owner]; - - if (pmi?.CanLoot == true) + if (map != null && map != Map.Internal) { - return false; + robe.MoveToWorld(from.Location, map); } } - return NotorietyHandlers.CorpseNotoriety(from, this) == Notoriety.Innocent; - } + var pack = from.Backpack; - public override bool CheckItemUse(Mobile from, Item item) => - base.CheckItemUse(from, item) && (item == this || CanLoot(from, item)); - - public override bool CheckLift(Mobile from, Item item, ref LRReason reject) => - base.CheckLift(from, item, ref reject) && CanLoot(from, item); - - public override void OnItemUsed(Mobile from, Item item) - { - base.OnItemUsed(from, item); - - if (item is Food) + if (RestoreEquip != null && pack != null) { - from.RevealingAction(); - } + var packItems = new List(pack.Items); // Only items in the top-level pack are re-equipped - if (item != this && IsCriminalAction(from)) - { - from.CriminalAction(true); - } - - if (!Looters.Contains(from)) - { - Looters.Add(from); - } - - m_InstancedItems?.Remove(item); - - } - - public override void OnItemLifted(Mobile from, Item item) - { - base.OnItemLifted(from, item); - - if (item != this && from != Owner) - { - from.RevealingAction(); - } - - if (item != this && IsCriminalAction(from)) - { - from.CriminalAction(true); - } - - if (!Looters.Contains(from)) - { - Looters.Add(from); - } - - m_InstancedItems?.Remove(item); - } - - public override void GetContextMenuEntries(Mobile from, List list) - { - base.GetContextMenuEntries(from, list); - - if (Core.AOS && Owner == from && from.Alive) - { - list.Add(new OpenCorpseEntry()); - } - } - - public bool GetRestoreInfo(Item item, ref Point3D loc) => - item != null && m_RestoreTable?.TryGetValue(item, out loc) == true; - - public void SetRestoreInfo(Item item, Point3D loc) - { - if (item == null) - { - return; - } - - m_RestoreTable ??= new Dictionary(); - - m_RestoreTable[item] = loc; - } - - public void ClearRestoreInfo(Item item) - { - if (m_RestoreTable == null || item == null) - { - return; - } - - m_RestoreTable.Remove(item); - - if (m_RestoreTable.Count == 0) - { - m_RestoreTable = null; - } - } - - public bool CanLoot(Mobile from, Item item) => - !IsCriminalAction(from) || (Map.Rules & MapRules.HarmfulRestrictions) == 0; - - public bool CheckLoot(Mobile from, Item item) - { - if (!CanLoot(from, item)) - { - if (Owner?.Player != true) + for (var i = 0; i < packItems.Count; i++) { - from.SendLocalizedMessage(1005035); // You did not earn the right to loot this creature! + var packItem = packItems[i]; + + if (RestoreEquip.Contains(packItem) && packItem.Movable) + { + from.EquipItem(packItem); + } + } + } + + var items = new List(Items); + + var didntFit = false; + + for (var i = 0; !didntFit && i < items.Count; ++i) + { + var item = items[i]; + var loc = item.Location; + + if (item.Layer is Layer.Hair or Layer.FacialHair || !item.Movable || !GetRestoreInfo(item, ref loc)) + { + continue; + } + + if (pack?.CheckHold(from, item, false, true) == true) + { + item.Location = loc; + pack.AddItem(item); + + if (RestoreEquip?.Contains(item) == true) + { + from.EquipItem(item); + } } else { - from.SendLocalizedMessage(1010049); // You may not loot this corpse. + didntFit = true; } - - return false; } - if (IsCriminalAction(from)) + from.PlaySound(0x3E3); + + if (Items.Count != 0) { - if (Owner?.Player != true) + from.SendLocalizedMessage(1062472); // You gather some of your belongings. The rest remain on the corpse. + } + else + { + SetFlag(CorpseFlag.Carved, true); + + if (ItemID == 0x2006) { - from.SendLocalizedMessage(1005036); // Looting this monster corpse will be a criminal act! + ProcessDelta(); + SendRemovePacket(); + ItemID = Utility.Random(0xECA, 9); // bone graphic + Hue = 0; + ProcessDelta(); + } + + from.SendLocalizedMessage(1062471); // You quickly gather all of your belongings. + } + + SetFlag(CorpseFlag.SelfLooted, true); + } + + if (!CheckLoot(from, null)) + { + return; + } + + if (from is not PlayerMobile player) + { + return; + } + + var qs = player.Quest; + + if (qs is UzeraanTurmoilQuest) + { + var obj = qs.FindObjective(); + if (obj?.CorpseWithBone == this && (!obj.Completed || UzeraanTurmoilQuest.HasLostDaemonBone(player))) + { + Item bone = new QuestDaemonBone(); + + if (player.PlaceInBackpack(bone)) + { + obj.CorpseWithBone = null; + // You rummage through the bones and find a Daemon Bone! You quickly place the item in your pack. + player.SendLocalizedMessage(1049341, "", 0x22); + + if (!obj.Completed) + { + obj.Complete(); + } } else { - from.SendLocalizedMessage(1005038); // Looting this corpse will be a criminal act! + bone.Delete(); + // Rummaging through the bones you find a Daemon Bone, but can't pick it up because your pack is too full. Come back when you have more room in your pack. + player.SendLocalizedMessage(1049342, "", 0x22); } - } - return true; - } - - public virtual void Open(Mobile from, bool checkSelfLoot) - { - if (from.AccessLevel <= AccessLevel.Player && !from.InRange(GetWorldLocation(), 2)) - { - from.SendLocalizedMessage(500446); // That is too far away. return; } - - if (checkSelfLoot && from == Owner && !GetFlag(CorpseFlag.SelfLooted) && Items.Count != 0) + } + else if (qs is TheSummoningQuest) + { + var obj = qs.FindObjective(); + if (obj?.Completed == true && obj.CorpseWithSkull == this) { - if (from.FindItemOnLayer(Layer.OuterTorso) is DeathRobe robe) + var sk = new GoldenSkull(); + + if (player.PlaceInBackpack(sk)) { - var map = from.Map; - - if (map != null && map != Map.Internal) - { - robe.MoveToWorld(from.Location, map); - } - } - - var pack = from.Backpack; - - if (RestoreEquip != null && pack != null) - { - var packItems = new List(pack.Items); // Only items in the top-level pack are re-equipped - - for (var i = 0; i < packItems.Count; i++) - { - var packItem = packItems[i]; - - if (RestoreEquip.Contains(packItem) && packItem.Movable) - { - from.EquipItem(packItem); - } - } - } - - var items = new List(Items); - - var didntFit = false; - - for (var i = 0; !didntFit && i < items.Count; ++i) - { - var item = items[i]; - var loc = item.Location; - - if (item.Layer is Layer.Hair or Layer.FacialHair || !item.Movable || !GetRestoreInfo(item, ref loc)) - { - continue; - } - - if (pack?.CheckHold(from, item, false, true) == true) - { - item.Location = loc; - pack.AddItem(item); - - if (RestoreEquip?.Contains(item) == true) - { - from.EquipItem(item); - } - } - else - { - didntFit = true; - } - } - - from.PlaySound(0x3E3); - - if (Items.Count != 0) - { - from.SendLocalizedMessage(1062472); // You gather some of your belongings. The rest remain on the corpse. + obj.CorpseWithSkull = null; + // For your valor in combating the devourer, you have been awarded a golden skull. + player.SendLocalizedMessage(1050022); + qs.Complete(); } else { - SetFlag(CorpseFlag.Carved, true); - - if (ItemID == 0x2006) - { - ProcessDelta(); - SendRemovePacket(); - ItemID = Utility.Random(0xECA, 9); // bone graphic - Hue = 0; - ProcessDelta(); - } - - from.SendLocalizedMessage(1062471); // You quickly gather all of your belongings. - } - - SetFlag(CorpseFlag.SelfLooted, true); - } - - if (!CheckLoot(from, null)) - { - return; - } - - if (from is not PlayerMobile player) - { - return; - } - - var qs = player.Quest; - - if (qs is UzeraanTurmoilQuest) - { - var obj = qs.FindObjective(); - if (obj?.CorpseWithBone == this && (!obj.Completed || UzeraanTurmoilQuest.HasLostDaemonBone(player))) - { - Item bone = new QuestDaemonBone(); - - if (player.PlaceInBackpack(bone)) - { - obj.CorpseWithBone = null; - player.SendLocalizedMessage( - 1049341, - "", - 0x22 - ); // You rummage through the bones and find a Daemon Bone! You quickly place the item in your pack. - - if (!obj.Completed) - { - obj.Complete(); - } - } - else - { - bone.Delete(); - player.SendLocalizedMessage( - 1049342, - "", - 0x22 - ); // Rummaging through the bones you find a Daemon Bone, but can't pick it up because your pack is too full. Come back when you have more room in your pack. - } - - return; + sk.Delete(); + // You find a golden skull, but your backpack is too full to carry it. + player.SendLocalizedMessage(1050023); } } - else if (qs is TheSummoningQuest) - { - var obj = qs.FindObjective(); - if (obj?.Completed == true && obj.CorpseWithSkull == this) - { - var sk = new GoldenSkull(); - - if (player.PlaceInBackpack(sk)) - { - obj.CorpseWithSkull = null; - player.SendLocalizedMessage( - 1050022 - ); // For your valor in combating the devourer, you have been awarded a golden skull. - qs.Complete(); - } - else - { - sk.Delete(); - player.SendLocalizedMessage( - 1050023 - ); // You find a golden skull, but your backpack is too full to carry it. - } - } - } - - base.OnDoubleClick(from); } - public override void OnDoubleClick(Mobile from) - { - Open(from, Core.AOS); - } + base.OnDoubleClick(from); + } - public override bool CheckContentDisplay(Mobile from) => false; + public override void OnDoubleClick(Mobile from) + { + Open(from, Core.AOS); + } - public override void AddNameProperty(IPropertyList list) + public override bool CheckContentDisplay(Mobile from) => false; + + public override void AddNameProperty(IPropertyList list) + { + if (ItemID == 0x2006) // Corpse form { - if (ItemID == 0x2006) // Corpse form + if (_corpseName != null) { - if (m_CorpseName != null) - { - list.Add(m_CorpseName); - } - else - { - list.Add(1046414, Name); // the remains of ~1_NAME~ - } + list.Add(_corpseName); } - else // Bone form + else { list.Add(1046414, Name); // the remains of ~1_NAME~ } } - - public override void OnAosSingleClick(Mobile from) + else // Bone form { - var hue = Notoriety.GetHue(NotorietyHandlers.CorpseNotoriety(from, this)); - var opl = PropertyList; - - if (opl.Header > 0) - { - from.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Label, hue, 3, opl.Header, Name, opl.HeaderArgs); - } + list.Add(1046414, Name); // the remains of ~1_NAME~ } + } - public override void OnSingleClick(Mobile from) + public override void OnAosSingleClick(Mobile from) + { + var hue = Notoriety.GetHue(NotorietyHandlers.CorpseNotoriety(from, this)); + var opl = PropertyList; + + if (opl.Header > 0) { - var hue = Notoriety.GetHue(NotorietyHandlers.CorpseNotoriety(from, this)); + from.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Label, hue, 3, opl.Header, Name, opl.HeaderArgs); + } + } - if (ItemID == 0x2006) // Corpse form + public override void OnSingleClick(Mobile from) + { + var hue = Notoriety.GetHue(NotorietyHandlers.CorpseNotoriety(from, this)); + + if (ItemID == 0x2006) // Corpse form + { + if (_corpseName != null) { - if (m_CorpseName != null) - { - from.NetState.SendMessage(Serial, ItemID, MessageType.Label, hue, 3, true, null, "", m_CorpseName); - } - else - { - from.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Label, hue, 3, 1046414, "", Name); - } + from.NetState.SendMessage(Serial, ItemID, MessageType.Label, hue, 3, true, null, "", _corpseName); } - else // Bone form + else { from.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Label, hue, 3, 1046414, "", Name); } } - - private class InstancedItemInfo + else // Bone form { - private readonly Item m_Item; - private readonly Mobile m_Mobile; + from.NetState.SendMessageLocalized(Serial, ItemID, MessageType.Label, hue, 3, 1046414, "", Name); + } + } - public InstancedItemInfo(Item i, Mobile m) - { - m_Item = i; - m_Mobile = m; - } + private class InstancedItemInfo + { + private Item _item; + private Mobile _mobile; - public bool Perpetual { get; set; } - - public bool IsOwner(Mobile m) - { - if (m_Item.LootType == LootType.Cursed) // Cursed Items are part of everyone's instanced corpse... (?) - { - return true; - } - - if (m == null) - { - return false; // sanity - } - - if (m_Mobile == m) - { - return true; - } - - var myParty = Party.Get(m_Mobile); - - return myParty != null && myParty == Party.Get(m); - } + public InstancedItemInfo(Item i, Mobile m) + { + _item = i; + _mobile = m; } - private class InternalTimer : Timer + public bool Perpetual { get; set; } + + public bool IsOwner(Mobile m) { - private readonly Corpse m_Corpse; - - public InternalTimer(Corpse c, TimeSpan delay) : base(delay) => m_Corpse = c; - - protected override void OnTick() + if (_item.LootType == LootType.Cursed) // Cursed Items are part of everyone's instanced corpse... (?) { - if (!m_Corpse.GetFlag(CorpseFlag.NoBones)) - { - m_Corpse.TurnToBones(); - } - else - { - m_Corpse.Delete(); - } + return true; + } + + if (m == null) + { + return false; // sanity + } + + if (_mobile == m) + { + return true; + } + + var myParty = Party.Get(_mobile); + + return myParty != null && myParty == Party.Get(m); + } + } + + private class InternalTimer : Timer + { + private Corpse _corpse; + + public InternalTimer(Corpse c, TimeSpan delay) : base(delay) => _corpse = c; + + protected override void OnTick() + { + if (!_corpse.GetFlag(CorpseFlag.NoBones)) + { + _corpse.TurnToBones(); + } + else + { + _corpse.Delete(); } } + } - private class OpenCorpseEntry : ContextMenuEntry + private class OpenCorpseEntry : ContextMenuEntry + { + public OpenCorpseEntry() : base(6215, 2) { - public OpenCorpseEntry() : base(6215, 2) - { - } + } - public override void OnClick() + public override void OnClick() + { + if (Owner.Target is Corpse corpse && Owner.From.CheckAlive()) { - if (Owner.Target is Corpse corpse && Owner.From.CheckAlive()) - { - corpse.Open(Owner.From, false); - } + corpse.Open(Owner.From, false); } } } diff --git a/Projects/UOContent/Items/Misc/Corpses/CorpseNameAttribute.cs b/Projects/UOContent/Items/Misc/Corpses/CorpseNameAttribute.cs index a923345fd..5ba3339f7 100644 --- a/Projects/UOContent/Items/Misc/Corpses/CorpseNameAttribute.cs +++ b/Projects/UOContent/Items/Misc/Corpses/CorpseNameAttribute.cs @@ -1,12 +1,11 @@ using System; -namespace Server -{ - [AttributeUsage(AttributeTargets.Class)] - public class CorpseNameAttribute : Attribute - { - public CorpseNameAttribute(string name) => Name = name; +namespace Server; - public string Name { get; } - } +[AttributeUsage(AttributeTargets.Class)] +public class CorpseNameAttribute : Attribute +{ + public CorpseNameAttribute(string name) => Name = name; + + public string Name { get; } } diff --git a/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs b/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs index 8299b372e..c6331876a 100644 --- a/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs +++ b/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs @@ -17,143 +17,142 @@ using System.Buffers; using System.IO; using Server.Items; -namespace Server.Network +namespace Server.Network; + +public static class CorpsePackets { - public static class CorpsePackets + public static void SendCorpseEquip(this NetState ns, Mobile beholder, Corpse beheld) { - public static void SendCorpseEquip(this NetState ns, Mobile beholder, Corpse beheld) + if (ns.CannotSendPackets()) { - if (ns.CannotSendPackets()) + return; + } + + var list = beheld.EquipItems; + + var maxLength = 8 + (list.Count + 2) * 5; + var writer = new SpanWriter(stackalloc byte[maxLength]); + writer.Write((byte)0x89); + writer.Seek(2, SeekOrigin.Current); + writer.Write(beheld.Serial); + + for (var i = 0; i < list.Count; ++i) + { + var item = list[i]; + + if (!item.Deleted && beholder.CanSee(item) && item.Parent == beheld) { - return; + writer.Write((byte)(item.Layer + 1)); + writer.Write(item.Serial); } + } - var list = beheld.EquipItems; + if (beheld.Hair?.ItemID > 0) + { + writer.Write((byte)(Layer.Hair + 1)); + writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); + } - var maxLength = 8 + (list.Count + 2) * 5; - var writer = new SpanWriter(stackalloc byte[maxLength]); - writer.Write((byte)0x89); - writer.Seek(2, SeekOrigin.Current); + if (beheld.FacialHair?.ItemID > 0) + { + writer.Write((byte)(Layer.FacialHair + 1)); + writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); + } + + writer.Write((byte)Layer.Invalid); + + writer.WritePacketLength(); + ns.Send(writer.Span); + } + + public static void SendCorpseContent(this NetState ns, Mobile beholder, Corpse beheld) + { + if (ns.CannotSendPackets()) + { + return; + } + + var list = beheld.EquipItems; + var hairItemID = beheld.Hair?.ItemID ?? 0; + var facialHairItemID = beheld.FacialHair?.ItemID ?? 0; + var count = list.Count; + if (hairItemID > 0) + { + count++; + } + + if (facialHairItemID > 0) + { + count++; + } + + var maxLength = 5 + count * (ns.ContainerGridLines ? 20 : 19); + var writer = new SpanWriter(stackalloc byte[maxLength]); + writer.Write((byte)0x3C); + writer.Seek(4, SeekOrigin.Current); // Length and Count + + var written = 0; + for (var i = 0; i < list.Count; ++i) + { + var child = list[i]; + + if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) + { + writer.Write(child.Serial); + writer.Write((ushort)child.ItemID); + writer.Write((byte)0); // signed, itemID offset + writer.Write((ushort)child.Amount); + writer.Write((short)child.X); + writer.Write((short)child.Y); + if (ns.ContainerGridLines) + { + writer.Write((byte)0); // Grid Location? + } + writer.Write(beheld.Serial); + writer.Write((ushort)child.Hue); + + ++written; + } + } + + if (hairItemID > 0) + { + writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); + writer.Write((ushort)hairItemID); + writer.Write((byte)0); // signed, itemID offset + writer.Write((ushort)1); + writer.Write(0); // X/Y + if (ns.ContainerGridLines) + { + writer.Write((byte)0); // Grid Location? + } writer.Write(beheld.Serial); + writer.Write((ushort)beheld.Hair!.Hue); - for (var i = 0; i < list.Count; ++i) - { - var item = list[i]; - - if (!item.Deleted && beholder.CanSee(item) && item.Parent == beheld) - { - writer.Write((byte)(item.Layer + 1)); - writer.Write(item.Serial); - } - } - - if (beheld.Hair?.ItemID > 0) - { - writer.Write((byte)(Layer.Hair + 1)); - writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - } - - if (beheld.FacialHair?.ItemID > 0) - { - writer.Write((byte)(Layer.FacialHair + 1)); - writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - } - - writer.Write((byte)Layer.Invalid); - - writer.WritePacketLength(); - ns.Send(writer.Span); + ++written; } - public static void SendCorpseContent(this NetState ns, Mobile beholder, Corpse beheld) + if (facialHairItemID > 0) { - if (ns.CannotSendPackets()) + writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); + writer.Write((ushort)facialHairItemID); + writer.Write((byte)0); // signed, itemID offset + writer.Write((ushort)1); + writer.Write(0); // X/Y + if (ns.ContainerGridLines) { - return; + writer.Write((byte)0); // Grid Location? } + writer.Write(beheld.Serial); + writer.Write((ushort)beheld.FacialHair!.Hue); - var list = beheld.EquipItems; - var hairItemID = beheld.Hair?.ItemID ?? 0; - var facialHairItemID = beheld.FacialHair?.ItemID ?? 0; - var count = list.Count; - if (hairItemID > 0) - { - count++; - } - - if (facialHairItemID > 0) - { - count++; - } - - var maxLength = 5 + count * (ns.ContainerGridLines ? 20 : 19); - var writer = new SpanWriter(stackalloc byte[maxLength]); - writer.Write((byte)0x3C); - writer.Seek(4, SeekOrigin.Current); // Length and Count - - var written = 0; - for (var i = 0; i < list.Count; ++i) - { - var child = list[i]; - - if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) - { - writer.Write(child.Serial); - writer.Write((ushort)child.ItemID); - writer.Write((byte)0); // signed, itemID offset - writer.Write((ushort)child.Amount); - writer.Write((short)child.X); - writer.Write((short)child.Y); - if (ns.ContainerGridLines) - { - writer.Write((byte)0); // Grid Location? - } - writer.Write(beheld.Serial); - writer.Write((ushort)child.Hue); - - ++written; - } - } - - if (hairItemID > 0) - { - writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - writer.Write((ushort)hairItemID); - writer.Write((byte)0); // signed, itemID offset - writer.Write((ushort)1); - writer.Write(0); // X/Y - if (ns.ContainerGridLines) - { - writer.Write((byte)0); // Grid Location? - } - writer.Write(beheld.Serial); - writer.Write((ushort)beheld.Hair!.Hue); - - ++written; - } - - if (facialHairItemID > 0) - { - writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - writer.Write((ushort)facialHairItemID); - writer.Write((byte)0); // signed, itemID offset - writer.Write((ushort)1); - writer.Write(0); // X/Y - if (ns.ContainerGridLines) - { - writer.Write((byte)0); // Grid Location? - } - writer.Write(beheld.Serial); - writer.Write((ushort)beheld.FacialHair!.Hue); - - ++written; - } - - writer.Seek(1, SeekOrigin.Begin); - writer.Write((ushort)writer.BytesWritten); - writer.Write((ushort)written); - writer.Seek(0, SeekOrigin.End); - ns.Send(writer.Span); + ++written; } + + writer.Seek(1, SeekOrigin.Begin); + writer.Write((ushort)writer.BytesWritten); + writer.Write((ushort)written); + writer.Seek(0, SeekOrigin.End); + ns.Send(writer.Span); } } diff --git a/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs b/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs index a2a9f5f83..ead812e2f 100644 --- a/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/DecayedCorpse.cs @@ -1,108 +1,74 @@ using System; +using ModernUO.Serialization; -namespace Server.Items +namespace Server.Items; + +[SerializationGenerator(2, false)] +public partial class DecayedCorpse : Container { - public class DecayedCorpse : Container + private static TimeSpan _defaultDecayTime = TimeSpan.FromMinutes(7.0); + + [TimerDrift] + [SerializableField(0, getter: "private", setter: "private")] + private Timer _decayTimer; + + [DeserializeTimerField(0)] + private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); + + public DecayedCorpse(string name) : base(Utility.Random(0xECA, 9)) { - private static readonly TimeSpan m_DefaultDecayTime = TimeSpan.FromMinutes(7.0); - private DateTime m_DecayTime; - private Timer m_DecayTimer; + Movable = false; + Name = name; - public DecayedCorpse(string name) : base(Utility.Random(0xECA, 9)) + BeginDecay(_defaultDecayTime); + } + + // Do not display (x items, y stones) + public override bool DisplaysContent => false; + + public void BeginDecay(TimeSpan delay) + { + _decayTimer?.Stop(); + DecayTimer = new InternalTimer(this, delay); + DecayTimer.Start(); + } + + public override void OnAfterDelete() + { + _decayTimer?.Stop(); + _decayTimer = null; + } + + // Do not display (x items, y stones) + public override bool CheckContentDisplay(Mobile from) => false; + + public override void AddNameProperty(IPropertyList list) + { + list.Add(1046414, Name); // the remains of ~1_NAME~ + } + + public override void OnSingleClick(Mobile from) + { + LabelTo(from, 1046414, Name); // the remains of ~1_NAME~ + } + + private void Deserialize(IGenericReader reader, int version) + { + if (reader.ReadBool()) { - Movable = false; - Name = name; - - BeginDecay(m_DefaultDecayTime); + BeginDecay(reader.ReadDeltaTime() - Core.Now); } + } - public DecayedCorpse(Serial serial) : base(serial) + private class InternalTimer : Timer + { + private DecayedCorpse _corpse; + + public InternalTimer(DecayedCorpse c, TimeSpan delay) : base(delay) => _corpse = c; + + protected override void OnTick() { - } - - // Do not display (x items, y stones) - public override bool DisplaysContent => false; - - public void BeginDecay(TimeSpan delay) - { - m_DecayTimer?.Stop(); - - m_DecayTime = Core.Now + delay; - - m_DecayTimer = new InternalTimer(this, delay); - m_DecayTimer.Start(); - } - - public override void OnAfterDelete() - { - m_DecayTimer?.Stop(); - - m_DecayTimer = null; - } - - // Do not display (x items, y stones) - public override bool CheckContentDisplay(Mobile from) => false; - - public override void AddNameProperty(IPropertyList list) - { - list.Add(1046414, Name); // the remains of ~1_NAME~ - } - - public override void OnSingleClick(Mobile from) - { - LabelTo(from, 1046414, Name); // the remains of ~1_NAME~ - } - - public override void Serialize(IGenericWriter writer) - { - base.Serialize(writer); - - writer.Write(1); // version - - writer.Write(m_DecayTimer != null); - - if (m_DecayTimer != null) - { - writer.WriteDeltaTime(m_DecayTime); - } - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - BeginDecay(m_DefaultDecayTime); - - break; - } - case 1: - { - if (reader.ReadBool()) - { - BeginDecay(reader.ReadDeltaTime() - Core.Now); - } - - break; - } - } - } - - private class InternalTimer : Timer - { - private readonly DecayedCorpse m_Corpse; - - public InternalTimer(DecayedCorpse c, TimeSpan delay) : base(delay) => m_Corpse = c; - - protected override void OnTick() - { - m_Corpse.Delete(); - } + _corpse.Delete(); } } } diff --git a/Projects/UOContent/Migrations/Server.Items.Corpse.v13.json b/Projects/UOContent/Migrations/Server.Items.Corpse.v13.json new file mode 100644 index 000000000..1579dd24e --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.Corpse.v13.json @@ -0,0 +1,113 @@ +{ + "version": 13, + "type": "Server.Items.Corpse", + "properties": [ + { + "name": "RestoreEquip", + "type": "System.Collections.Generic.List\u003CServer.Item\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Item", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "Flags", + "type": "Server.Items.CorpseFlag", + "rule": "EnumMigrationRule" + }, + { + "name": "TimeOfDeath", + "type": "System.DateTime", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "DeltaTime" + ] + }, + { + "name": "RestoreTable", + "type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Point3D\u003E", + "rule": "DictionaryMigrationRule", + "ruleArguments": [ + "Server.Item", + "SerializableInterfaceMigrationRule", + "0", + "Server.Point3D", + "PrimitiveUOTypeMigrationRule", + "1", + "Point3D" + ] + }, + { + "name": "DecayTimer", + "type": "Server.Timer", + "rule": "TimerMigrationRule", + "ruleArguments": [ + "@TimerDrift" + ] + }, + { + "name": "Looters", + "type": "System.Collections.Generic.HashSet\u003CServer.Mobile\u003E", + "rule": "HashSetMigrationRule", + "ruleArguments": [ + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "Killer", + "type": "Server.Mobile", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Aggressors", + "type": "System.Collections.Generic.List\u003CServer.Mobile\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Mobile", + "SerializableInterfaceMigrationRule" + ] + }, + { + "name": "Owner", + "type": "Server.Mobile", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "CorpseName", + "type": "string", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "AccessLevel", + "type": "Server.AccessLevel", + "rule": "EnumMigrationRule" + }, + { + "name": "Guild", + "type": "Server.Guilds.Guild", + "rule": "SerializableInterfaceMigrationRule" + }, + { + "name": "Kills", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "EquipItems", + "type": "System.Collections.Generic.List\u003CServer.Item\u003E", + "rule": "ListMigrationRule", + "ruleArguments": [ + "Server.Item", + "SerializableInterfaceMigrationRule" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v2.json b/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v2.json new file mode 100644 index 000000000..fbabaecdd --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.DecayedCorpse.v2.json @@ -0,0 +1,14 @@ +{ + "version": 2, + "type": "Server.Items.DecayedCorpse", + "properties": [ + { + "name": "DecayTimer", + "type": "Server.Timer", + "rule": "TimerMigrationRule", + "ruleArguments": [ + "@TimerDrift" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Skills/ForensicEval.cs b/Projects/UOContent/Skills/ForensicEval.cs index 24ad06a9a..1b82311fa 100644 --- a/Projects/UOContent/Skills/ForensicEval.cs +++ b/Projects/UOContent/Skills/ForensicEval.cs @@ -46,50 +46,53 @@ namespace Server.SkillHandlers } else { - from.SendLocalizedMessage(501001); // You cannot determain anything useful. + from.SendLocalizedMessage(501001); // You cannot determine anything useful. } } else if (target is Corpse c) { if (from.CheckTargetSkill(SkillName.Forensics, c, 0.0, 100.0)) { - if (c.m_Forensicist != null) + if (c.Forensicist != null) { - from.SendLocalizedMessage( - 1042750, - c.m_Forensicist - ); // The forensicist ~1_NAME~ has already discovered that: + // The forensicist ~1_NAME~ has already discovered that: + from.SendLocalizedMessage(1042750, c.Forensicist); } else { - c.m_Forensicist = from.Name; + c.Forensicist = from.Name; } if (((Body)c.Amount).IsHuman) { - from.SendLocalizedMessage( - 1042751, - c.Killer == null ? "no one" : c.Killer.Name - ); // This person was killed by ~1_KILLER_NAME~ + // This person was killed by ~1_KILLER_NAME~ + from.SendLocalizedMessage(1042751, c.Killer == null ? "no one" : c.Killer.Name); } if (c.Looters.Count > 0) { using var sb = new ValueStringBuilder(stackalloc char[128]); - for (var i = 0; i < c.Looters.Count; i++) + int i = 0; + foreach (var looter in c.Looters) { - if (i > 0) + if (i == c.Looters.Count - 1) { - sb.Append(i == c.Looters.Count - 1 ? ", and " : ", "); + sb.Append($", and {looter.Name}"); + } + else if (i > 0) + { + sb.Append($", {looter.Name}"); + } + else + { + sb.Append(looter.Name); } - sb.Append(c.Looters[i].Name); + i++; } - from.SendLocalizedMessage( - 1042752, - sb.ToString() - ); // This body has been disturbed by ~1_PLAYER_NAMES~ + // This body has been disturbed by ~1_PLAYER_NAMES~ + from.SendLocalizedMessage(1042752, sb.ToString()); } else { @@ -98,7 +101,7 @@ namespace Server.SkillHandlers } else { - from.SendLocalizedMessage(501001); // You cannot determain anything useful. + from.SendLocalizedMessage(501001); // You cannot determine anything useful. } } else if (target is ILockpickable p)