Fixes #2462 ## Summary Removes the per-object `VirtualHairInfo` heap wrapper for mobile/corpse hair. Hair is now stored **inline** on `Mobile` and `Corpse` as `int _hairItemId` / `int _hairHue` plus a lazily-allocated, **non-serialized** ephemeral `Serial _hairSerial` (in the high virtual-serial range) — and likewise for facial hair. The `VirtualHairInfo` class is deleted, with a **lossless** save migration. This delivers three things: 1. **Fixes a hair-removal bug.** `Delta(MobileDelta.Hair)` is deferred (it enqueues; `ProcessDeltaQueue` runs later in the tick). The old `HairItemID = 0` setter nulled `_hair` *immediately*, so by the time `ProcessDelta` built the remove packet the equipped virtual serial was already gone — the old `??=` code then re-materialized a **fresh** serial (≠ the equipped one), so clients never removed the right entity, and it left a phantom ItemId-0 object behind. The serial now lives on the entity and **persists across removal**, so remove packets carry the correct serial. 2. **Lightens the entity.** No heap hair object; bald mobiles allocate nothing (the serial is minted lazily only when hair is present). This was the original reason `HairItemID`/`HairHue` exist. 3. **Removes `VirtualHairInfo` entirely**, keeping the high-range virtual serial behavior. ## How - **Mobile** (manual serialization): inline `_hairItemId/_hairHue/_hairSerial` (+facial); lazy `HairSerial`/`FacialHairSerial`; `ProcessDelta` reads those. Serialization **v36 → v37** — the v30-v37 deserialize is unified, reading the legacy per-hair `VirtualHairInfo` version int only when `version < 37`. Setting item id to 0 clears the hue (matching the old object-nulling) while retaining the serial. - **Corpse** (codegen serialization): decomposed to `[SerializableField] int _hairItemId/_hairHue` (+facial) + ephemeral serial; **v16 → v17** with `MigrateFrom(V16Content)`. - **Lossless migration:** the loader validates exact byte length, and the old corpse hair is a presence-bool-gated block, so a tiny **migration-only** `LegacyHairInfo` reader (no runtime role) consumes the legacy `[bool][int ver][int itemId][int hue]` bytes. Frozen `Corpse.v14/v15/v16.json` are retyped to it; `v17.json` describes the new int fields. - All consumers updated to discrete accessors: `OutgoingMobilePackets`, `CorpsePackets`, corpse subclasses (`MilitiaFighterCorpse`, `SchmendrickApprenticeCorpse`), and the packet test mirrors. - `VirtualHair.cs` renamed to `OutgoingVirtualHairPackets.cs` (the only type left in it after `VirtualHairInfo` was removed). ## Test Plan - [x] Full solution build: **0 warnings, 0 errors** (`TreatWarningsAsErrors`). - [x] `Server.Tests`: **708 passed** (incl. new `RemoveHairUsesEquippedSerial` / `RemoveFacialHairUsesEquippedSerial` proving the serial survives removal + hue clears). - [x] `UOContent.Tests` corpse/hair: **6 passed** (incl. `CorpseHairMigrationTests` asserting the legacy hair bytes are consumed exactly — the loader's length invariant). - [x] Generated migration code inspected: V14/V15/V16 readers consume the legacy block byte-for-byte; serial never written to disk. ## Upgrade notes - Old Mobile (v30–v36) and Corpse (v13–v16) saves load losslessly. - Minor cosmetic-only change: `SchmendrickApprenticeCorpse` hair/facial-hair RNG draws shift order within each pair (same draw count); irrelevant for a quest NPC corpse.
This commit is contained in:
parent
978f314f0e
commit
a8acfa31f8
18 changed files with 525 additions and 210 deletions
|
|
@ -504,16 +504,15 @@ public sealed class MobileIncoming : Packet
|
|||
|
||||
var eq = beheld.Items;
|
||||
var count = eq.Count;
|
||||
var hair = beheld.Hair;
|
||||
var facialHair = beheld.FacialHair;
|
||||
var hairItemId = beheld.HairItemID;
|
||||
var facialHairItemId = beheld.FacialHairItemID;
|
||||
|
||||
|
||||
if (hair != null)
|
||||
if (hairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (facialHair != null)
|
||||
if (facialHairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
|
@ -573,19 +572,19 @@ public sealed class MobileIncoming : Packet
|
|||
}
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
if (hairItemId > 0)
|
||||
{
|
||||
if (m_DupedLayers![(int)Layer.Hair] != m_Version)
|
||||
{
|
||||
m_DupedLayers[(int)Layer.Hair] = m_Version;
|
||||
hue = hair.Hue;
|
||||
hue = beheld.HairHue;
|
||||
|
||||
if (beheld.SolidHueOverride >= 0)
|
||||
{
|
||||
hue = beheld.SolidHueOverride;
|
||||
}
|
||||
|
||||
var itemID = hair.ItemId & itemIdMask;
|
||||
var itemID = hairItemId & itemIdMask;
|
||||
var writeHue = newPacket || hue != 0;
|
||||
|
||||
if (!newPacket && writeHue)
|
||||
|
|
@ -593,7 +592,7 @@ public sealed class MobileIncoming : Packet
|
|||
itemID |= 0x8000;
|
||||
}
|
||||
|
||||
Stream.Write(hair.VirtualSerial);
|
||||
Stream.Write(beheld.HairSerial);
|
||||
Stream.Write((ushort)itemID);
|
||||
Stream.Write((byte)Layer.Hair);
|
||||
|
||||
|
|
@ -604,19 +603,19 @@ public sealed class MobileIncoming : Packet
|
|||
}
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (facialHairItemId > 0)
|
||||
{
|
||||
if (m_DupedLayers![(int)Layer.FacialHair] != m_Version)
|
||||
{
|
||||
m_DupedLayers[(int)Layer.FacialHair] = m_Version;
|
||||
hue = facialHair.Hue;
|
||||
hue = beheld.FacialHairHue;
|
||||
|
||||
if (beheld.SolidHueOverride >= 0)
|
||||
{
|
||||
hue = beheld.SolidHueOverride;
|
||||
}
|
||||
|
||||
var itemID = facialHair.ItemId & itemIdMask;
|
||||
var itemID = facialHairItemId & itemIdMask;
|
||||
var writeHue = newPacket || hue != 0;
|
||||
|
||||
if (!newPacket && writeHue)
|
||||
|
|
@ -624,7 +623,7 @@ public sealed class MobileIncoming : Packet
|
|||
itemID |= 0x8000;
|
||||
}
|
||||
|
||||
Stream.Write(facialHair.VirtualSerial);
|
||||
Stream.Write(beheld.FacialHairSerial);
|
||||
Stream.Write((ushort)itemID);
|
||||
Stream.Write((byte)Layer.FacialHair);
|
||||
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ public class VirtualHairPacketTests
|
|||
var expected = new HairEquipUpdate(m).Compile();
|
||||
|
||||
using var ns = PacketTestUtilities.CreateTestNetState();
|
||||
ns.SendHairEquipUpdatePacket(m, (uint)m.Hair.VirtualSerial, m.Hair.ItemId, m.Hair.Hue, Layer.Hair);
|
||||
ns.SendHairEquipUpdatePacket(m, (uint)m.HairSerial, m.HairItemID, m.HairHue, Layer.Hair);
|
||||
|
||||
var result = ns.SendBuffer.GetReadSpan();
|
||||
AssertThat.Equal(result, expected);
|
||||
|
|
@ -33,9 +33,47 @@ public class VirtualHairPacketTests
|
|||
var expected = new RemoveHair(m).Compile();
|
||||
|
||||
using var ns = PacketTestUtilities.CreateTestNetState();
|
||||
ns.SendRemoveHairPacket((uint) m.Hair.VirtualSerial);
|
||||
ns.SendRemoveHairPacket((uint)m.HairSerial);
|
||||
|
||||
var result = ns.SendBuffer.GetReadSpan();
|
||||
AssertThat.Equal(result, expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveHairUsesEquippedSerial()
|
||||
{
|
||||
var m = new Mobile((Serial)0x1025u);
|
||||
m.DefaultMobileInit();
|
||||
m.HairItemID = 0x2000;
|
||||
m.HairHue = 0x1000;
|
||||
|
||||
var equippedSerial = m.HairSerial;
|
||||
Assert.NotEqual(Serial.Zero, equippedSerial);
|
||||
|
||||
m.HairItemID = 0; // remove
|
||||
|
||||
// Serial must survive removal so the client can remove the correct entity.
|
||||
Assert.Equal(equippedSerial, m.HairSerial);
|
||||
// Hue is cleared with the item id (matches the legacy object-nulling behavior).
|
||||
Assert.Equal(0, m.HairHue);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void RemoveFacialHairUsesEquippedSerial()
|
||||
{
|
||||
var m = new Mobile((Serial)0x1026u);
|
||||
m.DefaultMobileInit();
|
||||
m.FacialHairItemID = 0x2040;
|
||||
m.FacialHairHue = 0x1000;
|
||||
|
||||
var equippedSerial = m.FacialHairSerial;
|
||||
Assert.NotEqual(Serial.Zero, equippedSerial);
|
||||
|
||||
m.FacialHairItemID = 0; // remove
|
||||
|
||||
// Serial must survive removal so the client can remove the correct entity.
|
||||
Assert.Equal(equippedSerial, m.FacialHairSerial);
|
||||
// Hue is cleared with the item id (matches the legacy object-nulling behavior).
|
||||
Assert.Equal(0, m.FacialHairHue);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ public sealed class HairEquipUpdate : Packet
|
|||
{
|
||||
var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.HairHue;
|
||||
|
||||
Stream.Write(parent.Hair.VirtualSerial);
|
||||
Stream.Write(parent.HairSerial);
|
||||
Stream.Write((short)parent.HairItemID);
|
||||
Stream.Write((byte)0);
|
||||
Stream.Write((byte)Layer.Hair);
|
||||
|
|
@ -23,7 +23,7 @@ public sealed class FacialHairEquipUpdate : Packet
|
|||
{
|
||||
var hue = parent.SolidHueOverride >= 0 ? parent.SolidHueOverride : parent.FacialHairHue;
|
||||
|
||||
Stream.Write(parent.FacialHair.VirtualSerial);
|
||||
Stream.Write(parent.FacialHairSerial);
|
||||
Stream.Write((short)parent.FacialHairItemID);
|
||||
Stream.Write((byte)0);
|
||||
Stream.Write((byte)Layer.FacialHair);
|
||||
|
|
@ -37,7 +37,7 @@ public sealed class RemoveHair : Packet
|
|||
public RemoveHair(Mobile parent)
|
||||
: base(0x1D, 5)
|
||||
{
|
||||
Stream.Write(parent.Hair.VirtualSerial);
|
||||
Stream.Write(parent.HairSerial);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -46,6 +46,6 @@ public sealed class RemoveFacialHair : Packet
|
|||
public RemoveFacialHair(Mobile parent)
|
||||
: base(0x1D, 5)
|
||||
{
|
||||
Stream.Write(parent.FacialHair.VirtualSerial);
|
||||
Stream.Write(parent.FacialHairSerial);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
using System;
|
||||
using System.Buffers;
|
||||
using ModernUO.Serialization;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server;
|
||||
|
|
@ -64,34 +63,3 @@ public static class OutgoingVirtualHairPackets
|
|||
writer.Write(hairSerial);
|
||||
}
|
||||
}
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
public partial class VirtualHairInfo
|
||||
{
|
||||
[SerializableField(0)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _itemId;
|
||||
|
||||
[SerializableField(1)]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _hue;
|
||||
|
||||
public VirtualHairInfo() : this(0)
|
||||
{
|
||||
}
|
||||
|
||||
public VirtualHairInfo(int itemid, int hue = 0)
|
||||
{
|
||||
_itemId = itemid;
|
||||
_hue = hue;
|
||||
VirtualSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
[AfterDeserialization]
|
||||
private void AfterDeserialization()
|
||||
{
|
||||
VirtualSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
public Serial VirtualSerial { get; private set; }
|
||||
}
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"version": 0,
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"properties": [
|
||||
{
|
||||
"name": "ItemId",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Hue",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -272,8 +272,9 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
private TimerExecutionToken _expireAggrTimerToken;
|
||||
private TimerExecutionToken _expireCombatantTimerToken;
|
||||
private TimerExecutionToken _expireCriminalTimerToken;
|
||||
private VirtualHairInfo _facialHair;
|
||||
public VirtualHairInfo FacialHair => _facialHair ??= new VirtualHairInfo(FacialHairItemID, FacialHairHue);
|
||||
private int _facialHairItemId;
|
||||
private int _facialHairHue;
|
||||
private Serial _facialHairSerial;
|
||||
|
||||
private int m_Fame, m_Karma;
|
||||
private bool m_Female, m_Warmode, m_Hidden, m_Blessed, m_Flying;
|
||||
|
|
@ -283,8 +284,9 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
private BaseGuild m_Guild;
|
||||
private string m_GuildTitle;
|
||||
|
||||
private VirtualHairInfo _hair;
|
||||
public VirtualHairInfo Hair => _hair ??= new VirtualHairInfo(HairItemID, HairHue);
|
||||
private int _hairItemId;
|
||||
private int _hairHue;
|
||||
private Serial _hairSerial;
|
||||
|
||||
private int m_Hits, m_Stam, m_Mana;
|
||||
|
||||
|
|
@ -2181,20 +2183,16 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int HairItemID
|
||||
{
|
||||
get => _hair?.ItemId ?? 0;
|
||||
get => _hairItemId;
|
||||
set
|
||||
{
|
||||
if (_hair == null && value > 0)
|
||||
// An item id of 0 is the "no hair" state; clear the hue with it, but keep
|
||||
// _hairSerial so a pending removal packet references the equipped serial.
|
||||
_hairItemId = value < 0 ? 0 : value;
|
||||
|
||||
if (_hairItemId == 0)
|
||||
{
|
||||
_hair = new VirtualHairInfo(value);
|
||||
}
|
||||
else if (value <= 0)
|
||||
{
|
||||
_hair = null;
|
||||
}
|
||||
else if (_hair != null)
|
||||
{
|
||||
_hair.ItemId = value;
|
||||
_hairHue = 0;
|
||||
}
|
||||
|
||||
Delta(MobileDelta.Hair);
|
||||
|
|
@ -2205,20 +2203,14 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int FacialHairItemID
|
||||
{
|
||||
get => _facialHair?.ItemId ?? 0;
|
||||
get => _facialHairItemId;
|
||||
set
|
||||
{
|
||||
if (_facialHair == null && value > 0)
|
||||
_facialHairItemId = value < 0 ? 0 : value;
|
||||
|
||||
if (_facialHairItemId == 0)
|
||||
{
|
||||
_facialHair = new VirtualHairInfo(value);
|
||||
}
|
||||
else if (value <= 0)
|
||||
{
|
||||
_facialHair = null;
|
||||
}
|
||||
else if (_facialHair != null)
|
||||
{
|
||||
_facialHair.ItemId = value;
|
||||
_facialHairHue = 0;
|
||||
}
|
||||
|
||||
Delta(MobileDelta.FacialHair);
|
||||
|
|
@ -2229,12 +2221,12 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int HairHue
|
||||
{
|
||||
get => _hair?.Hue ?? 0;
|
||||
get => _hairHue;
|
||||
set
|
||||
{
|
||||
if (_hair != null)
|
||||
if (_hairItemId > 0)
|
||||
{
|
||||
_hair.Hue = value;
|
||||
_hairHue = value;
|
||||
Delta(MobileDelta.Hair);
|
||||
this.MarkDirty();
|
||||
}
|
||||
|
|
@ -2244,18 +2236,44 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int FacialHairHue
|
||||
{
|
||||
get => _facialHair?.Hue ?? 0;
|
||||
get => _facialHairHue;
|
||||
set
|
||||
{
|
||||
if (_facialHair != null)
|
||||
if (_facialHairItemId > 0)
|
||||
{
|
||||
_facialHair.Hue = value;
|
||||
_facialHairHue = value;
|
||||
Delta(MobileDelta.FacialHair);
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Serial HairSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hairItemId > 0 && _hairSerial == Serial.Zero)
|
||||
{
|
||||
_hairSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
return _hairSerial;
|
||||
}
|
||||
}
|
||||
|
||||
public Serial FacialHairSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_facialHairItemId > 0 && _facialHairSerial == Serial.Zero)
|
||||
{
|
||||
_facialHairSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
return _facialHairSerial;
|
||||
}
|
||||
}
|
||||
|
||||
public Item ShieldArmor => FindItemOnLayer(Layer.TwoHanded);
|
||||
|
||||
public Item NeckArmor => FindItemOnLayer(Layer.Neck);
|
||||
|
|
@ -2296,7 +2314,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
public virtual void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.Write(36); // version
|
||||
writer.Write(37); // version
|
||||
|
||||
writer.WriteDeltaTime(LastStrGain);
|
||||
writer.WriteDeltaTime(LastIntGain);
|
||||
|
|
@ -2304,12 +2322,12 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
byte hairflag = 0x00;
|
||||
|
||||
if (_hair != null)
|
||||
if (_hairItemId > 0)
|
||||
{
|
||||
hairflag |= 0x01;
|
||||
}
|
||||
|
||||
if (_facialHair != null)
|
||||
if (_facialHairItemId > 0)
|
||||
{
|
||||
hairflag |= 0x02;
|
||||
}
|
||||
|
|
@ -2318,12 +2336,14 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
if ((hairflag & 0x01) != 0)
|
||||
{
|
||||
_hair?.Serialize(writer);
|
||||
writer.Write(_hairItemId);
|
||||
writer.Write(_hairHue);
|
||||
}
|
||||
|
||||
if ((hairflag & 0x02) != 0)
|
||||
{
|
||||
_facialHair?.Serialize(writer);
|
||||
writer.Write(_facialHairItemId);
|
||||
writer.Write(_facialHairHue);
|
||||
}
|
||||
|
||||
writer.Write(Race);
|
||||
|
|
@ -2458,8 +2478,6 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
m_Map?.OnLeave(this);
|
||||
m_Map = null;
|
||||
|
||||
_hair = null;
|
||||
_facialHair = null;
|
||||
m_MountItem = null;
|
||||
|
||||
World.RemoveEntity(this);
|
||||
|
|
@ -2734,14 +2752,14 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
sendFacialHair = true;
|
||||
}
|
||||
|
||||
var hairSerial = Hair.VirtualSerial;
|
||||
var hairSerial = HairSerial;
|
||||
var hairLength = removeHair
|
||||
? OutgoingVirtualHairPackets.RemovePacketLength
|
||||
: OutgoingVirtualHairPackets.EquipUpdatePacketLength;
|
||||
|
||||
var hairPacket = stackalloc byte[hairLength].InitializePacket();
|
||||
|
||||
var facialHairSerial = FacialHair.VirtualSerial;
|
||||
var facialHairSerial = FacialHairSerial;
|
||||
var facialHairLength = removeFacialHair
|
||||
? OutgoingVirtualHairPackets.RemovePacketLength
|
||||
: OutgoingVirtualHairPackets.EquipUpdatePacketLength;
|
||||
|
|
@ -6111,6 +6129,7 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
|
||||
switch (version)
|
||||
{
|
||||
case 37: // Decomposed hair into inline item id/hue (dropped the VirtualHairInfo object)
|
||||
case 36: // Moved virtues to VirtueSystem
|
||||
case 35: // Moved short term murders to PlayerMurderSystem
|
||||
case 34: // Moved Stabled to PlayerMobile
|
||||
|
|
@ -6126,18 +6145,30 @@ public partial class Mobile : IHued, IComparable<Mobile>, ISpawnable, IObjectPro
|
|||
}
|
||||
case 30:
|
||||
{
|
||||
// Before v37 each hair was a VirtualHairInfo whose Serialize wrote a
|
||||
// leading version int ahead of the item id and hue.
|
||||
var hairflag = reader.ReadByte();
|
||||
|
||||
if ((hairflag & 0x01) != 0)
|
||||
{
|
||||
_hair = new VirtualHairInfo();
|
||||
_hair.Deserialize(reader);
|
||||
if (version < 37)
|
||||
{
|
||||
reader.ReadInt(); // legacy VirtualHairInfo version
|
||||
}
|
||||
|
||||
_hairItemId = reader.ReadInt();
|
||||
_hairHue = reader.ReadInt();
|
||||
}
|
||||
|
||||
if ((hairflag & 0x02) != 0)
|
||||
{
|
||||
_facialHair = new VirtualHairInfo();
|
||||
_facialHair.Deserialize(reader);
|
||||
if (version < 37)
|
||||
{
|
||||
reader.ReadInt(); // legacy VirtualHairInfo version
|
||||
}
|
||||
|
||||
_facialHairItemId = reader.ReadInt();
|
||||
_facialHairHue = reader.ReadInt();
|
||||
}
|
||||
|
||||
goto case 29;
|
||||
|
|
|
|||
|
|
@ -674,7 +674,7 @@ public static class OutgoingMobilePackets
|
|||
itemID |= 0x8000;
|
||||
}
|
||||
|
||||
writer.Write(beheld.Hair.VirtualSerial);
|
||||
writer.Write(beheld.HairSerial);
|
||||
writer.Write((ushort)itemID);
|
||||
writer.Write((byte)Layer.Hair);
|
||||
|
||||
|
|
@ -697,7 +697,7 @@ public static class OutgoingMobilePackets
|
|||
itemID |= 0x8000;
|
||||
}
|
||||
|
||||
writer.Write(beheld.FacialHair.VirtualSerial);
|
||||
writer.Write(beheld.FacialHairSerial);
|
||||
writer.Write((ushort)itemID);
|
||||
writer.Write((byte)Layer.FacialHair);
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
using System;
|
||||
using Server;
|
||||
using Server.Items;
|
||||
using Xunit;
|
||||
|
||||
namespace UOContent.Tests;
|
||||
|
||||
// Verifies the migration read type LegacyHairInfo consumes the exact legacy on-disk
|
||||
// VirtualHairInfo block the way the generated Corpse migration does:
|
||||
// [bool present] then, if present, [int version][int itemId][int hue].
|
||||
// Dropping/over-reading these bytes would trip the loader's exact-length validation,
|
||||
// so full consumption (no leftover bytes) is the load-bearing assertion.
|
||||
public class CorpseHairMigrationTests
|
||||
{
|
||||
private static byte[] WriteLegacyHairTail(bool present, int itemId, int hue)
|
||||
{
|
||||
var writer = new BufferWriter(true);
|
||||
writer.Write(present);
|
||||
if (present)
|
||||
{
|
||||
writer.Write(0); // legacy VirtualHairInfo serialization version
|
||||
writer.Write(itemId);
|
||||
writer.Write(hue);
|
||||
}
|
||||
|
||||
var buffer = new byte[writer.Position];
|
||||
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x203B, 1102)]
|
||||
[InlineData(0x2049, 0)]
|
||||
public void LegacyHairTail_Present_RoundTrips(int itemId, int hue)
|
||||
{
|
||||
var buffer = WriteLegacyHairTail(true, itemId, hue);
|
||||
var reader = new BufferReader(buffer);
|
||||
|
||||
Assert.True(reader.ReadBool());
|
||||
var hair = new LegacyHairInfo();
|
||||
hair.Deserialize(reader);
|
||||
|
||||
Assert.Equal(itemId, hair.ItemId);
|
||||
Assert.Equal(hue, hair.Hue);
|
||||
// The entire block must be consumed - the loader validates exact byte length.
|
||||
Assert.Equal(buffer.Length, reader.Position);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void LegacyHairTail_Absent_ConsumesOnlyPresenceBool()
|
||||
{
|
||||
var buffer = WriteLegacyHairTail(false, 0, 0);
|
||||
var reader = new BufferReader(buffer);
|
||||
|
||||
Assert.False(reader.ReadBool());
|
||||
// Absent case: nothing else to read, and the presence bool was the only byte.
|
||||
Assert.Equal(buffer.Length, reader.Position);
|
||||
}
|
||||
}
|
||||
|
|
@ -10,15 +10,13 @@ public sealed class CorpseEquip : Packet
|
|||
var list = beheld.EquipItems;
|
||||
|
||||
var count = list.Count;
|
||||
var hair = beheld.Hair;
|
||||
var facialHair = beheld.FacialHair;
|
||||
|
||||
if (hair != null)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (facialHair != null)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
|
@ -38,16 +36,16 @@ public sealed class CorpseEquip : Packet
|
|||
}
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
Stream.Write((byte)(Layer.Hair + 1));
|
||||
Stream.Write(hair.VirtualSerial);
|
||||
Stream.Write(beheld.HairSerial);
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
Stream.Write((byte)(Layer.FacialHair + 1));
|
||||
Stream.Write(facialHair.VirtualSerial);
|
||||
Stream.Write(beheld.FacialHairSerial);
|
||||
}
|
||||
|
||||
Stream.Write((byte)Layer.Invalid);
|
||||
|
|
@ -61,15 +59,13 @@ public sealed class CorpseContent : Packet
|
|||
{
|
||||
var items = beheld.EquipItems;
|
||||
var count = items.Count;
|
||||
var hair = beheld.Hair;
|
||||
var facialHair = beheld.FacialHair;
|
||||
|
||||
if (hair != null)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (facialHair != null)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
|
@ -101,30 +97,30 @@ public sealed class CorpseContent : Packet
|
|||
}
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
Stream.Write(hair.VirtualSerial);
|
||||
Stream.Write((ushort)hair.ItemId);
|
||||
Stream.Write(beheld.HairSerial);
|
||||
Stream.Write((ushort)beheld.HairItemId);
|
||||
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);
|
||||
Stream.Write((ushort)beheld.HairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
Stream.Write(facialHair.VirtualSerial);
|
||||
Stream.Write((ushort)facialHair.ItemId);
|
||||
Stream.Write(beheld.FacialHairSerial);
|
||||
Stream.Write((ushort)beheld.FacialHairItemId);
|
||||
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);
|
||||
Stream.Write((ushort)beheld.FacialHairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
|
@ -141,15 +137,13 @@ public sealed class CorpseContent6017 : Packet
|
|||
{
|
||||
var items = beheld.EquipItems;
|
||||
var count = items.Count;
|
||||
var hair = beheld.Hair;
|
||||
var facialHair = beheld.FacialHair;
|
||||
|
||||
if (hair != null)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (facialHair != null)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
|
@ -182,32 +176,32 @@ public sealed class CorpseContent6017 : Packet
|
|||
}
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
Stream.Write(hair.VirtualSerial);
|
||||
Stream.Write((ushort)hair.ItemId);
|
||||
Stream.Write(beheld.HairSerial);
|
||||
Stream.Write((ushort)beheld.HairItemId);
|
||||
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);
|
||||
Stream.Write((ushort)beheld.HairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
Stream.Write(facialHair.VirtualSerial);
|
||||
Stream.Write((ushort)facialHair.ItemId);
|
||||
Stream.Write(beheld.FacialHairSerial);
|
||||
Stream.Write((ushort)beheld.FacialHairItemId);
|
||||
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);
|
||||
Stream.Write((ushort)beheld.FacialHairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,13 +10,18 @@ namespace Server.Engines.Quests.Haven;
|
|||
[SerializationGenerator(0, false)]
|
||||
public partial class SchmendrickApprenticeCorpse : Corpse
|
||||
{
|
||||
private static int _hairHue;
|
||||
|
||||
[SerializableField(0, setter: "private")]
|
||||
private Lantern _lantern;
|
||||
|
||||
[Constructible]
|
||||
public SchmendrickApprenticeCorpse() : base(GetOwner(), GetHair(), GetFacialHair(), GetEquipment())
|
||||
public SchmendrickApprenticeCorpse() : base(
|
||||
GetOwner(),
|
||||
Race.Human.RandomHair(false),
|
||||
Race.Human.RandomHairHue(),
|
||||
Race.Human.RandomFacialHair(false),
|
||||
Race.Human.RandomHairHue(),
|
||||
GetEquipment()
|
||||
)
|
||||
{
|
||||
Direction = Direction.West;
|
||||
|
||||
|
|
@ -53,19 +58,6 @@ public partial class SchmendrickApprenticeCorpse : Corpse
|
|||
new Spellbook()
|
||||
];
|
||||
|
||||
private static VirtualHairInfo GetHair()
|
||||
{
|
||||
_hairHue = Race.Human.RandomHairHue();
|
||||
return new VirtualHairInfo(Race.Human.RandomHair(false), _hairHue);
|
||||
}
|
||||
|
||||
private static VirtualHairInfo GetFacialHair()
|
||||
{
|
||||
_hairHue = Race.Human.RandomHairHue();
|
||||
|
||||
return new VirtualHairInfo(Race.Human.RandomFacialHair(false), _hairHue);
|
||||
}
|
||||
|
||||
public override void AddNameProperty(IPropertyList list)
|
||||
{
|
||||
if (ItemID == 0x2006) // Corpse form
|
||||
|
|
|
|||
|
|
@ -84,10 +84,14 @@ public partial class MilitiaFighter : BaseCreature
|
|||
[SerializationGenerator(0, false)]
|
||||
public partial class MilitiaFighterCorpse : Corpse
|
||||
{
|
||||
public MilitiaFighterCorpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialhair, List<Item> equipItems) : base(
|
||||
public MilitiaFighterCorpse(
|
||||
Mobile owner, int hairItemId, int hairHue, int facialHairItemId, int facialHairHue, List<Item> equipItems
|
||||
) : base(
|
||||
owner,
|
||||
hair,
|
||||
facialhair,
|
||||
hairItemId,
|
||||
hairHue,
|
||||
facialHairItemId,
|
||||
facialHairHue,
|
||||
equipItems
|
||||
)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ public enum CorpseFlag
|
|||
OwnerWasAnimatedDead = 0x00000800
|
||||
}
|
||||
|
||||
[SerializationGenerator(16, false)]
|
||||
[SerializationGenerator(17, false)]
|
||||
public partial class Corpse : Container, ICarvable
|
||||
{
|
||||
public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0);
|
||||
|
|
@ -150,24 +150,61 @@ public partial class Corpse : Container, ICarvable
|
|||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private List<Item> _equipItems;
|
||||
|
||||
[CanBeNull]
|
||||
[SerializableField(13, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private VirtualHairInfo _hair;
|
||||
private int _hairItemId;
|
||||
|
||||
[CanBeNull]
|
||||
[SerializableField(14, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private VirtualHairInfo _facialHair;
|
||||
private int _hairHue;
|
||||
|
||||
[SerializableField(15, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _facialHairItemId;
|
||||
|
||||
[SerializableField(16, setter: "private")]
|
||||
[SerializedCommandProperty(AccessLevel.GameMaster)]
|
||||
private int _facialHairHue;
|
||||
|
||||
private Serial _hairSerial;
|
||||
private Serial _facialHairSerial;
|
||||
|
||||
public Serial HairSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_hairItemId > 0 && _hairSerial == Serial.Zero)
|
||||
{
|
||||
_hairSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
return _hairSerial;
|
||||
}
|
||||
}
|
||||
|
||||
public Serial FacialHairSerial
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_facialHairItemId > 0 && _facialHairSerial == Serial.Zero)
|
||||
{
|
||||
_facialHairSerial = World.NewVirtual;
|
||||
}
|
||||
|
||||
return _facialHairSerial;
|
||||
}
|
||||
}
|
||||
|
||||
// Why was this public?
|
||||
// public override bool IsPublicContainer => true;
|
||||
|
||||
public Corpse(Mobile owner, List<Item> equipItems) : this(owner, null, null, equipItems)
|
||||
public Corpse(Mobile owner, List<Item> equipItems) : this(owner, 0, 0, 0, 0, equipItems)
|
||||
{
|
||||
}
|
||||
|
||||
public Corpse(Mobile owner, VirtualHairInfo hair, VirtualHairInfo facialHair, List<Item> equipItems)
|
||||
public Corpse(
|
||||
Mobile owner, int hairItemId, int hairHue, int facialHairItemId, int facialHairHue, List<Item> equipItems
|
||||
)
|
||||
: base(0x2006)
|
||||
{
|
||||
// To suppress console warnings, stackable must be true
|
||||
|
|
@ -202,14 +239,16 @@ public partial class Corpse : Container, ICarvable
|
|||
SetFlag(CorpseFlag.OwnerWasAnimatedDead, ownerBaseCreature.IsAnimatedDead);
|
||||
}
|
||||
|
||||
if (hair?.ItemId > 0)
|
||||
if (hairItemId > 0)
|
||||
{
|
||||
_hair = new VirtualHairInfo(hair.ItemId, hair.Hue);
|
||||
_hairItemId = hairItemId;
|
||||
_hairHue = hairHue;
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (facialHairItemId > 0)
|
||||
{
|
||||
_facialHair = new VirtualHairInfo(facialHair.ItemId, facialHair.Hue);
|
||||
_facialHairItemId = facialHairItemId;
|
||||
_facialHairHue = facialHairHue;
|
||||
}
|
||||
|
||||
// This corpse does not turn to bones if: the owner is not a player
|
||||
|
|
@ -279,6 +318,36 @@ public partial class Corpse : Container, ICarvable
|
|||
DevourCorpse();
|
||||
}
|
||||
|
||||
// Decomposed VirtualHairInfo into discrete int fields (hair/facial hair item id + hue)
|
||||
private void MigrateFrom(V16Content 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;
|
||||
_equipItems = content.EquipItems;
|
||||
if (content.Hair != null)
|
||||
{
|
||||
_hairItemId = content.Hair.ItemId;
|
||||
_hairHue = content.Hair.Hue;
|
||||
}
|
||||
|
||||
if (content.FacialHair != null)
|
||||
{
|
||||
_facialHairItemId = content.FacialHair.ItemId;
|
||||
_facialHairHue = content.FacialHair.Hue;
|
||||
}
|
||||
}
|
||||
|
||||
// Folded Murderer bool field into CorpseFlag.Murderer
|
||||
private void MigrateFrom(V15Content content)
|
||||
{
|
||||
|
|
@ -300,8 +369,17 @@ public partial class Corpse : Container, ICarvable
|
|||
_accessLevel = content.AccessLevel;
|
||||
_guild = content.Guild;
|
||||
_equipItems = content.EquipItems;
|
||||
_hair = content.Hair;
|
||||
_facialHair = content.FacialHair;
|
||||
if (content.Hair != null)
|
||||
{
|
||||
_hairItemId = content.Hair.ItemId;
|
||||
_hairHue = content.Hair.Hue;
|
||||
}
|
||||
|
||||
if (content.FacialHair != null)
|
||||
{
|
||||
_facialHairItemId = content.FacialHair.ItemId;
|
||||
_facialHairHue = content.FacialHair.Hue;
|
||||
}
|
||||
}
|
||||
|
||||
// Replaced int Kills snapshot with bool Murderer snapshot
|
||||
|
|
@ -325,8 +403,17 @@ public partial class Corpse : Container, ICarvable
|
|||
_accessLevel = content.AccessLevel;
|
||||
_guild = content.Guild;
|
||||
_equipItems = content.EquipItems;
|
||||
_hair = content.Hair;
|
||||
_facialHair = content.FacialHair;
|
||||
if (content.Hair != null)
|
||||
{
|
||||
_hairItemId = content.Hair.ItemId;
|
||||
_hairHue = content.Hair.Hue;
|
||||
}
|
||||
|
||||
if (content.FacialHair != null)
|
||||
{
|
||||
_facialHairItemId = content.FacialHair.ItemId;
|
||||
_facialHairHue = content.FacialHair.Hue;
|
||||
}
|
||||
}
|
||||
|
||||
// Added corpse hair and corpse facial hair
|
||||
|
|
@ -634,8 +721,22 @@ public partial class Corpse : Container, ICarvable
|
|||
public static Container Mobile_CreateCorpseHandler(Mobile owner, List<Item> initialContent, List<Item> equipItems)
|
||||
{
|
||||
var c = owner is MilitiaFighter
|
||||
? new MilitiaFighterCorpse(owner, owner.Hair, owner.FacialHair, equipItems)
|
||||
: new Corpse(owner, owner.Hair, owner.FacialHair, equipItems);
|
||||
? new MilitiaFighterCorpse(
|
||||
owner,
|
||||
owner.HairItemID,
|
||||
owner.HairHue,
|
||||
owner.FacialHairItemID,
|
||||
owner.FacialHairHue,
|
||||
equipItems
|
||||
)
|
||||
: new Corpse(
|
||||
owner,
|
||||
owner.HairItemID,
|
||||
owner.HairHue,
|
||||
owner.FacialHairItemID,
|
||||
owner.FacialHairHue,
|
||||
equipItems
|
||||
);
|
||||
|
||||
owner.Corpse = c;
|
||||
|
||||
|
|
|
|||
|
|
@ -49,16 +49,16 @@ public static class CorpsePackets
|
|||
|
||||
if (beheld.Owner != null)
|
||||
{
|
||||
if (beheld.Hair?.ItemId > 0)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
writer.Write((byte)(Layer.Hair + 1));
|
||||
writer.Write(beheld.Hair.VirtualSerial);
|
||||
writer.Write(beheld.HairSerial);
|
||||
}
|
||||
|
||||
if (beheld.FacialHair?.ItemId > 0)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
writer.Write((byte)(Layer.FacialHair + 1));
|
||||
writer.Write(beheld.FacialHair.VirtualSerial);
|
||||
writer.Write(beheld.FacialHairSerial);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -76,16 +76,14 @@ public static class CorpsePackets
|
|||
}
|
||||
|
||||
var list = beheld.EquipItems;
|
||||
var hair = beheld.Hair;
|
||||
var facialHair = beheld.FacialHair;
|
||||
|
||||
var count = list.Count;
|
||||
if (hair != null)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
||||
if (facialHair != null)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
|
|
@ -121,10 +119,10 @@ public static class CorpsePackets
|
|||
|
||||
if (beheld.Owner != null)
|
||||
{
|
||||
if (hair?.ItemId > 0)
|
||||
if (beheld.HairItemId > 0)
|
||||
{
|
||||
writer.Write(hair.VirtualSerial);
|
||||
writer.Write((ushort)hair.ItemId);
|
||||
writer.Write(beheld.HairSerial);
|
||||
writer.Write((ushort)beheld.HairItemId);
|
||||
writer.Write((byte)0); // signed, itemID offset
|
||||
writer.Write((ushort)1);
|
||||
writer.Write(0); // X/Y
|
||||
|
|
@ -133,15 +131,15 @@ public static class CorpsePackets
|
|||
writer.Write((byte)0); // Grid Location?
|
||||
}
|
||||
writer.Write(beheld.Serial);
|
||||
writer.Write((ushort)hair.Hue);
|
||||
writer.Write((ushort)beheld.HairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
||||
if (facialHair?.ItemId > 0)
|
||||
if (beheld.FacialHairItemId > 0)
|
||||
{
|
||||
writer.Write(facialHair.VirtualSerial);
|
||||
writer.Write((ushort)facialHair.ItemId);
|
||||
writer.Write(beheld.FacialHairSerial);
|
||||
writer.Write((ushort)beheld.FacialHairItemId);
|
||||
writer.Write((byte)0); // signed, itemID offset
|
||||
writer.Write((ushort)1);
|
||||
writer.Write(0); // X/Y
|
||||
|
|
@ -150,7 +148,7 @@ public static class CorpsePackets
|
|||
writer.Write((byte)0); // Grid Location?
|
||||
}
|
||||
writer.Write(beheld.Serial);
|
||||
writer.Write((ushort)facialHair.Hue);
|
||||
writer.Write((ushort)beheld.FacialHairHue);
|
||||
|
||||
++written;
|
||||
}
|
||||
|
|
|
|||
16
Projects/UOContent/Items/Misc/Corpses/LegacyHairInfo.cs
Normal file
16
Projects/UOContent/Items/Misc/Corpses/LegacyHairInfo.cs
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
namespace Server.Items;
|
||||
|
||||
// Reads the legacy on-disk VirtualHairInfo block ([int version][int itemId][int hue]).
|
||||
// Used ONLY by Corpse save migration (V14/V15/V16). It has no runtime role.
|
||||
public sealed class LegacyHairInfo
|
||||
{
|
||||
public int ItemId { get; private set; }
|
||||
public int Hue { get; private set; }
|
||||
|
||||
public void Deserialize(IGenericReader reader)
|
||||
{
|
||||
reader.ReadInt(); // legacy serialization version
|
||||
ItemId = reader.ReadInt();
|
||||
Hue = reader.ReadInt();
|
||||
}
|
||||
}
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Hair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
},
|
||||
{
|
||||
"name": "FacialHair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Hair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
@ -120,7 +120,7 @@
|
|||
},
|
||||
{
|
||||
"name": "FacialHair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@
|
|||
},
|
||||
{
|
||||
"name": "Hair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
@ -112,7 +112,7 @@
|
|||
},
|
||||
{
|
||||
"name": "FacialHair",
|
||||
"type": "Server.VirtualHairInfo",
|
||||
"type": "Server.Items.LegacyHairInfo",
|
||||
"rule": "RawSerializableMigrationRule",
|
||||
"ruleArguments": [
|
||||
"",
|
||||
|
|
|
|||
137
Projects/UOContent/Migrations/Server.Items.Corpse.v17.json
generated
Normal file
137
Projects/UOContent/Migrations/Server.Items.Corpse.v17.json
generated
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
{
|
||||
"version": 17,
|
||||
"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": "EquipItems",
|
||||
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
|
||||
"rule": "ListMigrationRule",
|
||||
"ruleArguments": [
|
||||
"Server.Item",
|
||||
"SerializableInterfaceMigrationRule"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HairItemId",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "HairHue",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FacialHairItemId",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "FacialHairHue",
|
||||
"type": "int",
|
||||
"rule": "PrimitiveTypeMigrationRule",
|
||||
"ruleArguments": [
|
||||
""
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue