diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePackets.cs index 565141eb2..a9ddb1175 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePackets.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/MobilePackets.cs @@ -504,13 +504,16 @@ namespace Server.Tests.Network var eq = beheld.Items; var count = eq.Count; + var hair = beheld.Hair; + var facialHair = beheld.FacialHair; - if (beheld.HairItemID > 0) + + if (hair != null) { count++; } - if (beheld.FacialHairItemID > 0) + if (facialHair != null) { count++; } @@ -570,19 +573,19 @@ namespace Server.Tests.Network } } - if (beheld.HairItemID > 0) + if (hair?.ItemId > 0) { if (m_DupedLayers![(int)Layer.Hair] != m_Version) { m_DupedLayers[(int)Layer.Hair] = m_Version; - hue = beheld.HairHue; + hue = hair.Hue; if (beheld.SolidHueOverride >= 0) { hue = beheld.SolidHueOverride; } - var itemID = beheld.HairItemID & itemIdMask; + var itemID = hair.ItemId & itemIdMask; var writeHue = newPacket || hue != 0; if (!newPacket && writeHue) @@ -590,7 +593,7 @@ namespace Server.Tests.Network itemID |= 0x8000; } - Stream.Write(HairInfo.FakeSerial(beheld.Serial)); + Stream.Write(hair.VirtualSerial); Stream.Write((ushort)itemID); Stream.Write((byte)Layer.Hair); @@ -601,19 +604,19 @@ namespace Server.Tests.Network } } - if (beheld.FacialHairItemID > 0) + if (facialHair?.ItemId > 0) { if (m_DupedLayers![(int)Layer.FacialHair] != m_Version) { m_DupedLayers[(int)Layer.FacialHair] = m_Version; - hue = beheld.FacialHairHue; + hue = facialHair.Hue; if (beheld.SolidHueOverride >= 0) { hue = beheld.SolidHueOverride; } - var itemID = beheld.FacialHairItemID & itemIdMask; + var itemID = facialHair.ItemId & itemIdMask; var writeHue = newPacket || hue != 0; if (!newPacket && writeHue) @@ -621,7 +624,7 @@ namespace Server.Tests.Network itemID |= 0x8000; } - Stream.Write(FacialHairInfo.FakeSerial(beheld.Serial)); + Stream.Write(facialHair.VirtualSerial); Stream.Write((ushort)itemID); Stream.Write((byte)Layer.FacialHair); diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs index 30139a7fe..13a6ac9c7 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPacketTests.cs @@ -17,7 +17,7 @@ namespace Server.Tests var expected = new HairEquipUpdate(m).Compile(); var ns = PacketTestUtilities.CreateTestNetState(); - ns.SendHairEquipUpdatePacket(m, HairInfo.FakeSerial(m.Serial), m.HairItemID, m.HairHue, Layer.Hair); + ns.SendHairEquipUpdatePacket(m, (uint)m.Hair.VirtualSerial, m.Hair.ItemId, m.Hair.Hue, Layer.Hair); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); @@ -32,7 +32,7 @@ namespace Server.Tests var expected = new RemoveHair(m).Compile(); var ns = PacketTestUtilities.CreateTestNetState(); - ns.SendRemoveHairPacket(HairInfo.FakeSerial(m.Serial)); + ns.SendRemoveHairPacket((uint) m.Hair.VirtualSerial); var result = ns.SendPipe.Reader.AvailableToRead(); AssertThat.Equal(result, expected); diff --git a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs index d4b425f52..a037f07bb 100644 --- a/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs +++ b/Projects/Server.Tests/Tests/Network/Packets/Outgoing/VirtualHairPackets.cs @@ -7,7 +7,7 @@ namespace Server.Network { var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.HairHue; - Stream.Write(HairInfo.FakeSerial(parent.Serial)); + Stream.Write(parent.Hair.VirtualSerial); Stream.Write((short)parent.HairItemID); Stream.Write((byte)0); Stream.Write((byte)Layer.Hair); @@ -23,7 +23,7 @@ namespace Server.Network { var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.FacialHairHue; - Stream.Write(FacialHairInfo.FakeSerial(parent.Serial)); + Stream.Write(parent.FacialHair.VirtualSerial); Stream.Write((short)parent.FacialHairItemID); Stream.Write((byte)0); Stream.Write((byte)Layer.FacialHair); @@ -37,7 +37,7 @@ namespace Server.Network public RemoveHair(Mobile parent) : base(0x1D, 5) { - Stream.Write(HairInfo.FakeSerial(parent.Serial)); + Stream.Write(parent.Hair.VirtualSerial); } } @@ -46,7 +46,7 @@ namespace Server.Network public RemoveFacialHair(Mobile parent) : base(0x1D, 5) { - Stream.Write(FacialHairInfo.FakeSerial(parent.Serial)); + Stream.Write(parent.FacialHair.VirtualSerial); } } } diff --git a/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs new file mode 100644 index 000000000..a8d0241e4 --- /dev/null +++ b/Projects/Server.Tests/Tests/World/VirtualSerialTests.cs @@ -0,0 +1,24 @@ +using Xunit; + +namespace Server.Tests; + +[Collection("Sequential Tests")] +public class VirtualSerialTests : IClassFixture +{ + [Fact] + public void TestNewVirtualGetsAndRollover() + { + // Acquire virtual serials until we hit the max + Serial lastSerial = World.NewVirtual; + do + { + Serial virtualSerial = World.NewVirtual; + Assert.Equal(lastSerial + 1, virtualSerial); + lastSerial = (Serial) (uint) virtualSerial; + } while (lastSerial != World.MaxVirtualSerial); + + // Next one should be MaxItemSerial (the first VirtualSerial) due to rollover + var nextSerial = World.NewVirtual; + Assert.Equal(nextSerial, (Serial)World.ResetVirtualSerial); + } +} diff --git a/Projects/Server/Items/VirtualHair.cs b/Projects/Server/Items/VirtualHair.cs index 0e3b2a178..89310e21a 100644 --- a/Projects/Server/Items/VirtualHair.cs +++ b/Projects/Server/Items/VirtualHair.cs @@ -1,6 +1,7 @@ using System; using System.Buffers; using System.Runtime.CompilerServices; +using ModernUO.Serialization; using Server.Network; namespace Server; @@ -65,83 +66,33 @@ public static class OutgoingVirtualHairPackets } } -public abstract class BaseHairInfo +[SerializationGenerator(0, false)] +public partial class VirtualHairInfo { - protected BaseHairInfo(int itemid, int hue = 0) + [SerializableField(0)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _itemId; + + [SerializableField(1)] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private int _hue; + + public VirtualHairInfo() : this(0) { - ItemID = itemid; - Hue = hue; } - protected BaseHairInfo(IGenericReader reader) + public VirtualHairInfo(int itemid, int hue = 0) { - var version = reader.ReadInt(); - - switch (version) - { - case 0: - { - ItemID = reader.ReadInt(); - Hue = reader.ReadInt(); - break; - } - } + _itemId = itemid; + _hue = hue; + VirtualSerial = World.NewVirtual; } - [CommandProperty(AccessLevel.GameMaster)] - public int ItemID { get; set; } - - [CommandProperty(AccessLevel.GameMaster)] - public int Hue { get; set; } - - public virtual void Serialize(IGenericWriter writer) + [AfterDeserialization] + private void AfterDeserialization() { - writer.Write(0); // version - writer.Write(ItemID); - writer.Write(Hue); + VirtualSerial = World.NewVirtual; } -} - -public class HairInfo : BaseHairInfo -{ - public HairInfo(int itemid) - : base(itemid) - { - } - - public HairInfo(int itemid, int hue) - : base(itemid, hue) - { - } - - public HairInfo(IGenericReader reader) - : base(reader) - { - } - - // TODO: Can we make this higher for newer clients? - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint FakeSerial(Serial m) => 0x7FFFFFFF - 0x400 - m.Value * 4; -} - -public class FacialHairInfo : BaseHairInfo -{ - public FacialHairInfo(int itemid) - : base(itemid) - { - } - - public FacialHairInfo(int itemid, int hue) - : base(itemid, hue) - { - } - - public FacialHairInfo(IGenericReader reader) - : base(reader) - { - } - - // TODO: Can we make this higher for newer clients? - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static uint FakeSerial(Serial m) => 0x7FFFFFFF - 0x400 - 1 - m.Value* 4; + + public Serial VirtualSerial { get; private set; } } diff --git a/Projects/Server/Migrations/Server.VirtualHairInfo.v0.json b/Projects/Server/Migrations/Server.VirtualHairInfo.v0.json new file mode 100644 index 000000000..1eaf27a0d --- /dev/null +++ b/Projects/Server/Migrations/Server.VirtualHairInfo.v0.json @@ -0,0 +1,22 @@ +{ + "version": 0, + "type": "Server.VirtualHairInfo", + "properties": [ + { + "name": "ItemId", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Hue", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index 588ce0cc9..978215c10 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -187,7 +187,7 @@ public delegate bool AllowBeneficialHandler(Mobile from, Mobile target); public delegate bool AllowHarmfulHandler(Mobile from, Mobile target); public delegate Container CreateCorpseHandler( - Mobile from, HairInfo hair, FacialHairInfo facialhair, List initialContent, List equippedItems + Mobile from, List initialContent, List equippedItems ); public delegate int AOSStatusHandler(Mobile from, int index); @@ -273,7 +273,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro private TimerExecutionToken _expireAggrTimerToken; private TimerExecutionToken _expireCombatantTimerToken; private TimerExecutionToken _expireCriminalTimerToken; - private FacialHairInfo m_FacialHair; + private VirtualHairInfo _facialHair; + public VirtualHairInfo FacialHair => _facialHair ??= new VirtualHairInfo(FacialHairItemID, FacialHairHue); + private int m_Fame, m_Karma; private bool m_Female, m_Warmode, m_Hidden, m_Blessed, m_Flying; private int m_Followers, m_FollowersMax; @@ -282,7 +284,9 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro private BaseGuild m_Guild; private string m_GuildTitle; - private HairInfo m_Hair; + private VirtualHairInfo _hair; + public VirtualHairInfo Hair => _hair ??= new VirtualHairInfo(HairItemID, HairHue); + private int m_Hits, m_Stam, m_Mana; private Item m_Holding; @@ -2165,59 +2169,62 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro [CommandProperty(AccessLevel.GameMaster)] public int HairItemID { - get => m_Hair?.ItemID ?? 0; + get => _hair?.ItemId ?? 0; set { - if (m_Hair == null && value > 0) + if (_hair == null && value > 0) { - m_Hair = new HairInfo(value); + _hair = new VirtualHairInfo(value); } else if (value <= 0) { - m_Hair = null; + _hair = null; } - else if (m_Hair != null) + else if (_hair != null) { - m_Hair.ItemID = value; + _hair.ItemId = value; } Delta(MobileDelta.Hair); + this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public int FacialHairItemID { - get => m_FacialHair?.ItemID ?? 0; + get => _facialHair?.ItemId ?? 0; set { - if (m_FacialHair == null && value > 0) + if (_facialHair == null && value > 0) { - m_FacialHair = new FacialHairInfo(value); + _facialHair = new VirtualHairInfo(value); } else if (value <= 0) { - m_FacialHair = null; + _facialHair = null; } - else if (m_FacialHair != null) + else if (_facialHair != null) { - m_FacialHair.ItemID = value; + _facialHair.ItemId = value; } Delta(MobileDelta.FacialHair); + this.MarkDirty(); } } [CommandProperty(AccessLevel.GameMaster)] public int HairHue { - get => m_Hair?.Hue ?? 0; + get => _hair?.Hue ?? 0; set { - if (m_Hair != null) + if (_hair != null) { - m_Hair.Hue = value; + _hair.Hue = value; Delta(MobileDelta.Hair); + this.MarkDirty(); } } } @@ -2225,13 +2232,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro [CommandProperty(AccessLevel.GameMaster)] public int FacialHairHue { - get => m_FacialHair?.Hue ?? 0; + get => _facialHair?.Hue ?? 0; set { - if (m_FacialHair != null) + if (_facialHair != null) { - m_FacialHair.Hue = value; + _facialHair.Hue = value; Delta(MobileDelta.FacialHair); + this.MarkDirty(); } } } @@ -2280,12 +2288,12 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro byte hairflag = 0x00; - if (m_Hair != null) + if (_hair != null) { hairflag |= 0x01; } - if (m_FacialHair != null) + if (_facialHair != null) { hairflag |= 0x02; } @@ -2294,12 +2302,12 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro if ((hairflag & 0x01) != 0) { - m_Hair?.Serialize(writer); + _hair?.Serialize(writer); } if ((hairflag & 0x02) != 0) { - m_FacialHair?.Serialize(writer); + _facialHair?.Serialize(writer); } writer.Write(Race); @@ -2434,8 +2442,8 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro m_Map?.OnLeave(this); m_Map = null; - m_Hair = null; - m_FacialHair = null; + _hair = null; + _facialHair = null; m_MountItem = null; World.RemoveEntity(this); @@ -2708,14 +2716,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro sendFacialHair = true; } - var hairSerial = HairInfo.FakeSerial(Serial); + var hairSerial = Hair.VirtualSerial; var hairLength = removeHair ? OutgoingVirtualHairPackets.RemovePacketLength : OutgoingVirtualHairPackets.EquipUpdatePacketLength; Span hairPacket = stackalloc byte[hairLength].InitializePacket(); - var facialHairSerial = FacialHairInfo.FakeSerial(Serial); + var facialHairSerial = FacialHair.VirtualSerial; var facialHairLength = removeFacialHair ? OutgoingVirtualHairPackets.RemovePacketLength : OutgoingVirtualHairPackets.EquipUpdatePacketLength; @@ -2806,14 +2814,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro { if (removeHair) { - OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, hairSerial); + OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial); } else { OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket( hairPacket, this, - hairSerial, + (uint)hairSerial, HairItemID, HairHue, Layer.Hair @@ -2827,14 +2835,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro { if (removeFacialHair) { - OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, facialHairSerial); + OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial); } else { OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket( facialHairPacket, this, - facialHairSerial, + (uint)facialHairSerial, FacialHairItemID, FacialHairHue, Layer.FacialHair @@ -2943,14 +2951,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro { if (removeHair) { - OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, hairSerial); + OutgoingVirtualHairPackets.CreateRemoveHairPacket(hairPacket, (uint)hairSerial); } else { OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket( hairPacket, this, - hairSerial, + (uint)hairSerial, HairItemID, HairHue, Layer.Hair @@ -2964,14 +2972,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro { if (removeFacialHair) { - OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, facialHairSerial); + OutgoingVirtualHairPackets.CreateRemoveHairPacket(facialHairPacket, (uint)facialHairSerial); } else { OutgoingVirtualHairPackets.CreateHairEquipUpdatePacket( facialHairPacket, this, - facialHairSerial, + (uint)facialHairSerial, FacialHairItemID, FacialHairHue, Layer.FacialHair @@ -4764,19 +4772,7 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro } } - HairInfo hair = null; - if (m_Hair != null) - { - hair = new HairInfo(m_Hair.ItemID, m_Hair.Hue); - } - - FacialHairInfo facialhair = null; - if (m_FacialHair != null) - { - facialhair = new FacialHairInfo(m_FacialHair.ItemID, m_FacialHair.Hue); - } - - var c = CreateCorpseHandler?.Invoke(this, hair, facialhair, content, equip); + var c = CreateCorpseHandler?.Invoke(this, content, equip); if (m_Map != null) { @@ -6092,12 +6088,14 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro if ((hairflag & 0x01) != 0) { - m_Hair = new HairInfo(reader); + _hair = new VirtualHairInfo(); + _hair.Deserialize(reader); } if ((hairflag & 0x02) != 0) { - m_FacialHair = new FacialHairInfo(reader); + _facialHair = new VirtualHairInfo(); + _facialHair.Deserialize(reader); } goto case 29; diff --git a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs index 669aa5a95..2e41e656d 100644 --- a/Projects/Server/Network/Packets/OutgoingMobilePackets.cs +++ b/Projects/Server/Network/Packets/OutgoingMobilePackets.cs @@ -672,7 +672,7 @@ public static class OutgoingMobilePackets itemID |= 0x8000; } - writer.Write(HairInfo.FakeSerial(beheld.Serial)); + writer.Write(beheld.Hair.VirtualSerial); writer.Write((ushort)itemID); writer.Write((byte)Layer.Hair); @@ -695,7 +695,7 @@ public static class OutgoingMobilePackets itemID |= 0x8000; } - writer.Write(FacialHairInfo.FakeSerial(beheld.Serial)); + writer.Write(beheld.FacialHair.VirtualSerial); writer.Write((ushort)itemID); writer.Write((byte)Layer.FacialHair); diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index fb0ac77ea..32659f751 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -53,13 +53,37 @@ public static class World public const bool DirtyTrackingEnabled = false; public const uint ItemOffset = 0x40000000; - public const uint MaxItemSerial = 0x7FFFFFFF; + public const uint MaxItemSerial = 0x7EEEEEEE; public const uint MaxMobileSerial = ItemOffset - 1; + public const uint ResetVirtualSerial = MaxItemSerial; + public const uint MaxVirtualSerial = 0x7FFFFFFF; + private static uint _nextVirtualSerial = ResetVirtualSerial; + public static Serial NewMobile => _mobilePersistence.NewEntity; public static Serial NewItem => _itemPersistence.NewEntity; public static Serial NewGuild => _guildPersistence.NewEntity; + // Virtual things don't persist across saves + public static Serial NewVirtual + { + get + { +#if THREADGUARD + if (Thread.CurrentThread != Core.Thread) + { + logger.Error( + "Attempted to get a new virtual serial from the wrong thread!\n{StackTrace}", + new StackTrace() + ); + } +#endif + var value = _nextVirtualSerial > MaxVirtualSerial ? ResetVirtualSerial : _nextVirtualSerial; + _nextVirtualSerial = value + 1; + return (Serial)value; + } + } + public static Dictionary Items => _itemPersistence.EntitiesBySerial; public static Dictionary Mobiles => _mobilePersistence.EntitiesBySerial; public static Dictionary Guilds => _guildPersistence.EntitiesBySerial; diff --git a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/Packets.cs b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/Packets.cs index 725203c8a..6dd973e58 100644 --- a/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/Packets.cs +++ b/Projects/UOContent.Tests/Tests/Items/Misc/Corpses/Packets.cs @@ -7,48 +7,51 @@ public sealed class CorpseEquip : Packet { public CorpseEquip(Mobile beholder, Corpse beheld) : base(0x89) { - var list = beheld.EquipItems; + var list = beheld.EquipItems; - var count = list.Count; - if (beheld.Hair?.ItemID > 0) - { - count++; - } + var count = list.Count; + var hair = beheld.Hair; + var facialHair = beheld.FacialHair; - if (beheld.FacialHair?.ItemID > 0) - { - count++; - } - - EnsureCapacity(8 + count * 5); - - Stream.Write(beheld.Serial); - - for (var i = 0; i < list.Count; ++i) - { - var item = list[i]; - - if (!item.Deleted && beholder.CanSee(item) && item.Parent == beheld) - { - Stream.Write((byte)(item.Layer + 1)); - Stream.Write(item.Serial); - } - } - - if (beheld.Hair?.ItemID > 0) - { - Stream.Write((byte)(Layer.Hair + 1)); - Stream.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - } - - if (beheld.FacialHair?.ItemID > 0) - { - Stream.Write((byte)(Layer.FacialHair + 1)); - Stream.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - } - - Stream.Write((byte)Layer.Invalid); + if (hair != null) + { + count++; } + + if (facialHair != null) + { + count++; + } + + EnsureCapacity(8 + count * 5); + + Stream.Write(beheld.Serial); + + for (var i = 0; i < list.Count; ++i) + { + var item = list[i]; + + if (!item.Deleted && beholder.CanSee(item) && item.Parent == beheld) + { + Stream.Write((byte)(item.Layer + 1)); + Stream.Write(item.Serial); + } + } + + if (hair?.ItemId > 0) + { + Stream.Write((byte)(Layer.Hair + 1)); + Stream.Write(hair.VirtualSerial); + } + + if (facialHair?.ItemId > 0) + { + Stream.Write((byte)(Layer.FacialHair + 1)); + Stream.Write(facialHair.VirtualSerial); + } + + Stream.Write((byte)Layer.Invalid); + } } public sealed class CorpseContent : Packet @@ -56,77 +59,79 @@ public sealed class CorpseContent : Packet public CorpseContent(Mobile beholder, Corpse beheld) : base(0x3C) { - var items = beheld.EquipItems; - var count = items.Count; + var items = beheld.EquipItems; + var count = items.Count; + var hair = beheld.Hair; + var facialHair = beheld.FacialHair; - if (beheld.Hair?.ItemID > 0) - { - count++; - } - - if (beheld.FacialHair?.ItemID > 0) - { - count++; - } - - EnsureCapacity(5 + count * 19); - - var pos = Stream.Position; - - var written = 0; - - Stream.Write((ushort)0); - - for (var i = 0; i < items.Count; ++i) - { - var child = items[i]; - - if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) - { - Stream.Write(child.Serial); - Stream.Write((ushort)child.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)child.Amount); - Stream.Write((short)child.X); - Stream.Write((short)child.Y); - Stream.Write(beheld.Serial); - Stream.Write((ushort)child.Hue); - - ++written; - } - } - - if (beheld.Hair?.ItemID > 0) - { - Stream.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - Stream.Write((ushort)beheld.Hair.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)1); - Stream.Write((short)0); - Stream.Write((short)0); - Stream.Write(beheld.Serial); - Stream.Write((ushort)beheld.Hair.Hue); - - ++written; - } - - if (beheld.FacialHair?.ItemID > 0) - { - Stream.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - Stream.Write((ushort)beheld.FacialHair.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)1); - Stream.Write((short)0); - Stream.Write((short)0); - Stream.Write(beheld.Serial); - Stream.Write((ushort)beheld.FacialHair.Hue); - - ++written; - } - - Stream.Seek(pos, SeekOrigin.Begin); - Stream.Write((ushort)written); + if (hair != null) + { + count++; } + + if (facialHair != null) + { + count++; + } + + EnsureCapacity(5 + count * 19); + + var pos = Stream.Position; + + var written = 0; + + Stream.Write((ushort)0); + + for (var i = 0; i < items.Count; ++i) + { + var child = items[i]; + + if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) + { + Stream.Write(child.Serial); + Stream.Write((ushort)child.ItemID); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)child.Amount); + Stream.Write((short)child.X); + Stream.Write((short)child.Y); + Stream.Write(beheld.Serial); + Stream.Write((ushort)child.Hue); + + ++written; + } + } + + if (hair?.ItemId > 0) + { + Stream.Write(hair.VirtualSerial); + Stream.Write((ushort)hair.ItemId); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)1); + Stream.Write((short)0); + Stream.Write((short)0); + Stream.Write(beheld.Serial); + Stream.Write((ushort)hair.Hue); + + ++written; + } + + if (facialHair?.ItemId > 0) + { + Stream.Write(facialHair.VirtualSerial); + Stream.Write((ushort)facialHair.ItemId); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)1); + Stream.Write((short)0); + Stream.Write((short)0); + Stream.Write(beheld.Serial); + Stream.Write((ushort)facialHair.Hue); + + ++written; + } + + Stream.Seek(pos, SeekOrigin.Begin); + Stream.Write((ushort)written); + } } public sealed class CorpseContent6017 : Packet @@ -134,78 +139,80 @@ public sealed class CorpseContent6017 : Packet public CorpseContent6017(Mobile beholder, Corpse beheld) : base(0x3C) { - var items = beheld.EquipItems; - var count = items.Count; + var items = beheld.EquipItems; + var count = items.Count; + var hair = beheld.Hair; + var facialHair = beheld.FacialHair; - if (beheld.Hair?.ItemID > 0) - { - count++; - } - - if (beheld.FacialHair?.ItemID > 0) - { - count++; - } - - EnsureCapacity(5 + count * 20); - - var pos = Stream.Position; - - var written = 0; - - Stream.Write((ushort)0); - - for (var i = 0; i < items.Count; ++i) - { - var child = items[i]; - - if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) - { - Stream.Write(child.Serial); - Stream.Write((ushort)child.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)child.Amount); - Stream.Write((short)child.X); - Stream.Write((short)child.Y); - Stream.Write((byte)0); // Grid Location? - Stream.Write(beheld.Serial); - Stream.Write((ushort)child.Hue); - - ++written; - } - } - - if (beheld.Hair?.ItemID > 0) - { - Stream.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - Stream.Write((ushort)beheld.Hair.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)1); - Stream.Write((short)0); - Stream.Write((short)0); - Stream.Write((byte)0); // Grid Location? - Stream.Write(beheld.Serial); - Stream.Write((ushort)beheld.Hair.Hue); - - ++written; - } - - if (beheld.FacialHair?.ItemID > 0) - { - Stream.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - Stream.Write((ushort)beheld.FacialHair.ItemID); - Stream.Write((byte)0); // signed, itemID offset - Stream.Write((ushort)1); - Stream.Write((short)0); - Stream.Write((short)0); - Stream.Write((byte)0); // Grid Location? - Stream.Write(beheld.Serial); - Stream.Write((ushort)beheld.FacialHair.Hue); - - ++written; - } - - Stream.Seek(pos, SeekOrigin.Begin); - Stream.Write((ushort)written); + if (hair != null) + { + count++; } -} \ No newline at end of file + + if (facialHair != null) + { + count++; + } + + EnsureCapacity(5 + count * 20); + + var pos = Stream.Position; + + var written = 0; + + Stream.Write((ushort)0); + + for (var i = 0; i < items.Count; ++i) + { + var child = items[i]; + + if (!child.Deleted && child.Parent == beheld && beholder.CanSee(child)) + { + Stream.Write(child.Serial); + Stream.Write((ushort)child.ItemID); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)child.Amount); + Stream.Write((short)child.X); + Stream.Write((short)child.Y); + Stream.Write((byte)0); // Grid Location? + Stream.Write(beheld.Serial); + Stream.Write((ushort)child.Hue); + + ++written; + } + } + + if (hair?.ItemId > 0) + { + Stream.Write(hair.VirtualSerial); + Stream.Write((ushort)hair.ItemId); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)1); + Stream.Write((short)0); + Stream.Write((short)0); + Stream.Write((byte)0); // Grid Location? + Stream.Write(beheld.Serial); + Stream.Write((ushort)hair.Hue); + + ++written; + } + + if (facialHair?.ItemId > 0) + { + Stream.Write(facialHair.VirtualSerial); + Stream.Write((ushort)facialHair.ItemId); + Stream.Write((byte)0); // signed, itemID offset + Stream.Write((ushort)1); + Stream.Write((short)0); + Stream.Write((short)0); + Stream.Write((byte)0); // Grid Location? + Stream.Write(beheld.Serial); + Stream.Write((ushort)facialHair.Hue); + + ++written; + } + + Stream.Seek(pos, SeekOrigin.Begin); + Stream.Write((ushort)written); + } +} diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs index 2f2b592c6..33793a202 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Items/SchmendrickApprenticeCorpse.cs @@ -51,17 +51,17 @@ public partial class SchmendrickApprenticeCorpse : Corpse new Spellbook() ]; - private static HairInfo GetHair() + private static VirtualHairInfo GetHair() { _hairHue = Race.Human.RandomHairHue(); - return new HairInfo(Race.Human.RandomHair(false), _hairHue); + return new VirtualHairInfo(Race.Human.RandomHair(false), _hairHue); } - private static FacialHairInfo GetFacialHair() + private static VirtualHairInfo GetFacialHair() { _hairHue = Race.Human.RandomHairHue(); - return new FacialHairInfo(Race.Human.RandomFacialHair(false), _hairHue); + return new VirtualHairInfo(Race.Human.RandomFacialHair(false), _hairHue); } public override void AddNameProperty(IPropertyList list) diff --git a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/MilitiaFighter.cs b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/MilitiaFighter.cs index d2af2e934..af38fbb09 100644 --- a/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/MilitiaFighter.cs +++ b/Projects/UOContent/Engines/Quests/Uzeraan Turmoil/Mobiles/MilitiaFighter.cs @@ -84,7 +84,7 @@ public partial class MilitiaFighter : BaseCreature [SerializationGenerator(0, false)] public partial class MilitiaFighterCorpse : Corpse { - public MilitiaFighterCorpse(Mobile owner, HairInfo hair, FacialHairInfo facialhair, List equipItems) : base( + public MilitiaFighterCorpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialhair, List equipItems) : base( owner, hair, facialhair, diff --git a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs index 1be063325..8ea7d1319 100644 --- a/Projects/UOContent/Items/Misc/Corpses/Corpse.cs +++ b/Projects/UOContent/Items/Misc/Corpses/Corpse.cs @@ -64,7 +64,7 @@ public enum CorpseFlag SelfLooted = 0x00000080 } -[SerializationGenerator(13, false)] +[SerializationGenerator(14, false)] public partial class Corpse : Container, ICarvable { public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0); @@ -132,6 +132,16 @@ public partial class Corpse : Container, ICarvable [SerializedCommandProperty(AccessLevel.GameMaster)] private List _equipItems; + [CanBeNull] + [SerializableField(14, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private VirtualHairInfo _hair; + + [CanBeNull] + [SerializableField(15, setter: "private")] + [SerializedCommandProperty(AccessLevel.GameMaster)] + private VirtualHairInfo _facialHair; + // Why was this public? // public override bool IsPublicContainer => true; @@ -139,7 +149,7 @@ public partial class Corpse : Container, ICarvable { } - public Corpse(Mobile owner, HairInfo hair, FacialHairInfo facialhair, List equipItems) + public Corpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialHair, List equipItems) : base(0x2006) { // To suppress console warnings, stackable must be true @@ -163,8 +173,15 @@ public partial class Corpse : Container, ICarvable _kills = owner.Kills; SetFlag(CorpseFlag.Criminal, owner.Criminal); - Hair = hair; - FacialHair = facialhair; + if (hair?.ItemId > 0) + { + _hair = new VirtualHairInfo(hair.ItemId, hair.Hue); + } + + if (facialHair?.ItemId > 0) + { + _facialHair = new VirtualHairInfo(facialHair.ItemId, facialHair.Hue); + } // This corpse does not turn to bones if: the owner is not a player SetFlag(CorpseFlag.NoBones, !owner.Player); @@ -237,6 +254,26 @@ public partial class Corpse : Container, ICarvable DevourCorpse(); } + // Added corpse hair and corpse facial hair + private void MigrateFrom(V13Content content) + { + _restoreEquip = content.RestoreEquip; + _flags = content.Flags; + _timeOfDeath = content.TimeOfDeath; + _restoreTable = content.RestoreTable; + _decayTimer = new InternalTimer(this, content.DecayTimerDelay); + _decayTimer.Start(); + _looters = content.Looters; + _killer = content.Killer; + _aggressors = content.Aggressors; + _owner = content.Owner; + _corpseName = content.CorpseName; + _accessLevel = content.AccessLevel; + _guild = content.Guild; + _kills = content.Kills; + _equipItems = content.EquipItems; + } + [CommandProperty(AccessLevel.GameMaster)] public virtual bool InstancedCorpse => Core.SE && Core.Now < TimeOfDeath + InstancedCorpseTime; @@ -248,10 +285,6 @@ public partial class Corpse : Container, ICarvable [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); @@ -492,14 +525,11 @@ public partial class Corpse : Container, ICarvable Mobile.CreateCorpseHandler += Mobile_CreateCorpseHandler; } - public static Container Mobile_CreateCorpseHandler( - Mobile owner, HairInfo hair, FacialHairInfo facialhair, - List initialContent, List equipItems - ) + public static Container Mobile_CreateCorpseHandler(Mobile owner, List initialContent, List equipItems) { var c = owner is MilitiaFighter - ? new MilitiaFighterCorpse(owner, hair, facialhair, equipItems) - : new Corpse(owner, hair, facialhair, equipItems); + ? new MilitiaFighterCorpse(owner, owner.Hair, owner.FacialHair, equipItems) + : new Corpse(owner, owner.Hair, owner.FacialHair, equipItems); owner.Corpse = c; diff --git a/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs b/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs index 5b9a1b139..2faa5d18b 100644 --- a/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs +++ b/Projects/UOContent/Items/Misc/Corpses/CorpsePackets.cs @@ -49,16 +49,16 @@ public static class CorpsePackets if (beheld.Owner != null) { - if (beheld.Hair?.ItemID > 0) + if (beheld.Hair?.ItemId > 0) { writer.Write((byte)(Layer.Hair + 1)); - writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); + writer.Write(beheld.Hair.VirtualSerial); } - if (beheld.FacialHair?.ItemID > 0) + if (beheld.FacialHair?.ItemId > 0) { writer.Write((byte)(Layer.FacialHair + 1)); - writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); + writer.Write(beheld.FacialHair.VirtualSerial); } } @@ -76,15 +76,16 @@ public static class CorpsePackets } var list = beheld.EquipItems; - var hairItemID = beheld.Hair?.ItemID ?? 0; - var facialHairItemID = beheld.FacialHair?.ItemID ?? 0; + var hair = beheld.Hair; + var facialHair = beheld.FacialHair; + var count = list.Count; - if (hairItemID > 0) + if (hair != null) { count++; } - if (facialHairItemID > 0) + if (facialHair != null) { count++; } @@ -120,10 +121,10 @@ public static class CorpsePackets if (beheld.Owner != null) { - if (hairItemID > 0) + if (hair?.ItemId > 0) { - writer.Write(HairInfo.FakeSerial(beheld.Owner.Serial) - 2); - writer.Write((ushort)hairItemID); + writer.Write(hair.VirtualSerial); + writer.Write((ushort)hair.ItemId); writer.Write((byte)0); // signed, itemID offset writer.Write((ushort)1); writer.Write(0); // X/Y @@ -132,15 +133,15 @@ public static class CorpsePackets writer.Write((byte)0); // Grid Location? } writer.Write(beheld.Serial); - writer.Write((ushort)beheld.Hair!.Hue); + writer.Write((ushort)hair.Hue); ++written; } - if (facialHairItemID > 0) + if (facialHair?.ItemId > 0) { - writer.Write(FacialHairInfo.FakeSerial(beheld.Owner.Serial) - 2); - writer.Write((ushort)facialHairItemID); + writer.Write(facialHair.VirtualSerial); + writer.Write((ushort)facialHair.ItemId); writer.Write((byte)0); // signed, itemID offset writer.Write((ushort)1); writer.Write(0); // X/Y @@ -149,7 +150,7 @@ public static class CorpsePackets writer.Write((byte)0); // Grid Location? } writer.Write(beheld.Serial); - writer.Write((ushort)beheld.FacialHair!.Hue); + writer.Write((ushort)facialHair.Hue); ++written; } diff --git a/Projects/UOContent/Migrations/Server.Items.Corpse.v14.json b/Projects/UOContent/Migrations/Server.Items.Corpse.v14.json new file mode 100644 index 000000000..f10f8c563 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Items.Corpse.v14.json @@ -0,0 +1,131 @@ +{ + "version": 14, + "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" + ] + }, + { + "name": "Hair", + "type": "Server.VirtualHairInfo", + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "", + "@CanBeNull" + ] + }, + { + "name": "FacialHair", + "type": "Server.VirtualHairInfo", + "rule": "RawSerializableMigrationRule", + "ruleArguments": [ + "", + "@CanBeNull" + ] + } + ] +} \ No newline at end of file