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

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