fix: Hair and facial hair "teleporting" when mobile dies several times (#1901)

### Summary

* Added World.NewVirtual for creating virtual serial numbers
* Reserved range 0x7EEEEEEE to 0x7FFFFFFF for virtual serials
* Hair and Facial hair (for mobiles) now use virtual serials instead of FakeSerial() functions
* Consolidated virtual hair to a single `VirtualHairInfo` class.

Corpse hair and facial hair now persists across save/load and hair and facial hair no longer teleport to newest corpse.
This commit is contained in:
mdodkins 2024-08-08 04:10:23 +01:00 committed by GitHub
parent 883d873c1c
commit 91e37fb8d4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 548 additions and 357 deletions

View file

@ -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);

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -0,0 +1,24 @@
using Xunit;
namespace Server.Tests;
[Collection("Sequential Tests")]
public class VirtualSerialTests : IClassFixture<ServerFixture>
{
[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);
}
}

View file

@ -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; }
}

View file

@ -0,0 +1,22 @@
{
"version": 0,
"type": "Server.VirtualHairInfo",
"properties": [
{
"name": "ItemId",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Hue",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -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<Item> initialContent, List<Item> equippedItems
Mobile from, List<Item> initialContent, List<Item> equippedItems
);
public delegate int AOSStatusHandler(Mobile from, int index);
@ -273,7 +273,9 @@ public partial class Mobile : IHued, IComparable<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, ISpawnable, IObjectPro
sendFacialHair = true;
}
var hairSerial = HairInfo.FakeSerial(Serial);
var hairSerial = Hair.VirtualSerial;
var hairLength = removeHair
? OutgoingVirtualHairPackets.RemovePacketLength
: OutgoingVirtualHairPackets.EquipUpdatePacketLength;
Span<byte> 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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<Mobile>, 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;

View file

@ -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);

View file

@ -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<Serial, Item> Items => _itemPersistence.EntitiesBySerial;
public static Dictionary<Serial, Mobile> Mobiles => _mobilePersistence.EntitiesBySerial;
public static Dictionary<Serial, BaseGuild> Guilds => _guildPersistence.EntitiesBySerial;

View file

@ -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++;
}
}
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);
}
}

View file

@ -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)

View file

@ -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<Item> equipItems) : base(
public MilitiaFighterCorpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialhair, List<Item> equipItems) : base(
owner,
hair,
facialhair,

View file

@ -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<Item> _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<Item> equipItems)
public Corpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialHair, List<Item> 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<Item> initialContent, List<Item> equipItems
)
public static Container Mobile_CreateCorpseHandler(Mobile owner, List<Item> initialContent, List<Item> 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;

View file

@ -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;
}

View file

@ -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"
]
}
]
}