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:
parent
883d873c1c
commit
91e37fb8d4
15 changed files with 548 additions and 357 deletions
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
|||
22
Projects/Server/Migrations/Server.VirtualHairInfo.v0.json
generated
Normal file
22
Projects/Server/Migrations/Server.VirtualHairInfo.v0.json
generated
Normal 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": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue