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