diff --git a/Projects/Server/Guild.cs b/Projects/Server/Guild.cs index 83f5c50bc..dcbca3d9f 100644 --- a/Projects/Server/Guild.cs +++ b/Projects/Server/Guild.cs @@ -50,9 +50,6 @@ public abstract class BaseGuild : ISerializable [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [CommandProperty(AccessLevel.GameMaster)] - DateTime ISerializable.LastSerialized { get; set; } = Core.Now; - long ISerializable.SavePosition { get; set; } = -1; BufferWriter ISerializable.SaveBuffer { get; set; } diff --git a/Projects/Server/IEntity.cs b/Projects/Server/IEntity.cs index 8b2bdb597..a35bdaa75 100644 --- a/Projects/Server/IEntity.cs +++ b/Projects/Server/IEntity.cs @@ -45,8 +45,6 @@ public class Entity : IEntity DateTime ISerializable.Created { get; set; } = Core.Now; - DateTime ISerializable.LastSerialized { get; set; } = DateTime.MaxValue; - long ISerializable.SavePosition { get; set; } = -1; BufferWriter ISerializable.SaveBuffer { get; set; } diff --git a/Projects/Server/Items/Item.cs b/Projects/Server/Items/Item.cs index 4dd5b13ad..7144fa79f 100644 --- a/Projects/Server/Items/Item.cs +++ b/Projects/Server/Items/Item.cs @@ -760,9 +760,6 @@ public class Item : IHued, IComparable, ISpawnable, IObjectPropertyListEnt [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [CommandProperty(AccessLevel.GameMaster)] - DateTime ISerializable.LastSerialized { get; set; } = Core.Now; - long ISerializable.SavePosition { get; set; } = -1; BufferWriter ISerializable.SaveBuffer { get; set; } diff --git a/Projects/Server/Mobiles/Mobile.cs b/Projects/Server/Mobiles/Mobile.cs index ced0936b6..abe126576 100644 --- a/Projects/Server/Mobiles/Mobile.cs +++ b/Projects/Server/Mobiles/Mobile.cs @@ -2258,9 +2258,6 @@ public partial class Mobile : IHued, IComparable, ISpawnable, IObjectPro [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [CommandProperty(AccessLevel.GameMaster)] - DateTime ISerializable.LastSerialized { get; set; } = Core.Now; - long ISerializable.SavePosition { get; set; } = -1; BufferWriter ISerializable.SaveBuffer { get; set; } diff --git a/Projects/Server/Serialization/GenericEntitySerialization.cs b/Projects/Server/Serialization/GenericEntitySerialization.cs new file mode 100644 index 000000000..6e34a0d9d --- /dev/null +++ b/Projects/Server/Serialization/GenericEntitySerialization.cs @@ -0,0 +1,209 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using Server.Logging; + +namespace Server; + +public class GenericEntitySerialization where T : class, ISerializable +{ + private static readonly ILogger logger = LogFactory.GetLogger(typeof(GenericEntitySerialization)); + + private static string _systemName; + private static Serial _lastEntitySerial; + private static readonly Dictionary _pendingAdd = new(); + private static readonly Dictionary _pendingDelete = new(); + private static Dictionary _entitiesBySerial = new(); + + public static void Configure(string systemName) + { + _systemName = systemName; + typeof(T).RegisterFindEntity(Find); + Persistence.Register(_systemName, Serialize, WriteSnapshot, Deserialize); + } + + internal static void Serialize() + { + EntityPersistence.SaveEntities( + _entitiesBySerial.Values, + entity => entity.Serialize(World.SerializedTypes) + ); + } + + internal static void WriteSnapshot(string basePath) + { + IIndexInfo indexInfo = new EntityTypeIndex(_systemName); + EntityPersistence.WriteEntities(indexInfo, _entitiesBySerial, basePath,World.SerializedTypes, out _); + } + + internal static void Deserialize(string path, Dictionary typesDb) + { + IIndexInfo indexInfo = new EntityTypeIndex(_systemName); + + _entitiesBySerial = EntityPersistence.LoadIndex(path, indexInfo, typesDb, out List> entities); + + if (_entitiesBySerial.Count > 0) + { + _lastEntitySerial = _entitiesBySerial.Keys.Max(); + } + + EntityPersistence.LoadData(path, indexInfo, typesDb, entities); + } + + public static Serial NewEntity + { + get + { +#if THREADGUARD + if (Thread.CurrentThread != Core.Thread) + { + logger.Error( + "Attempted to get a new entity serial from the wrong thread!\n{StackTrace}", + new StackTrace() + ); + } +#endif + var last = _lastEntitySerial; + + for (uint i = 0; i < uint.MaxValue; i++) + { + last++; + + if (FindEntity(last) == null) + { + return _lastEntitySerial = last; + } + } + + OutOfMemory("No serials left to allocate for BOBEntries"); + return Serial.MinusOne; + } + } + + public static void AddEntity(T entity) + { + var worldState = World.WorldState; + switch (worldState) + { + default: // Not Running + { + throw new Exception($"Added {entity.GetType().Name} before world load."); + } + case WorldState.Saving: + case WorldState.Loading: + case WorldState.WritingSave: + { + if (_pendingDelete.Remove(entity.Serial)) + { + logger.Warning("Deleted then added {Entity} during {WorldState} state.", entity.GetType().Name, worldState.ToString()); + } + + _pendingAdd[entity.Serial] = entity; + break; + } + case WorldState.Running: + { + ref var entityEntry = ref CollectionsMarshal.GetValueRefOrAddDefault(_entitiesBySerial, entity.Serial, out bool exists); + if (exists) + { + if (entityEntry == entity) + { + logger.Error( + $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", + entity.GetType().FullName, + entity.Serial, + new StackTrace() + ); + } + else + { + logger.Error( + $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", + entity.GetType().FullName, + entity.Serial, + entityEntry.GetType().FullName, + entityEntry.Serial, + new StackTrace() + ); + } + } + else + { + entityEntry = entity; + } + break; + } + } + } + + public static void RemoveEntity(T entity) + { + var worldState = World.WorldState; + switch (worldState) + { + default: // Not Running + { + throw new Exception($"Removed {entity.GetType().Name} before world load."); + } + case WorldState.Saving: + case WorldState.Loading: + case WorldState.WritingSave: + { + _pendingAdd.Remove(entity.Serial); + _pendingDelete[entity.Serial] = entity; + break; + } + case WorldState.Running: + { + _entitiesBySerial.Remove(entity.Serial); + break; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Find(Serial serial) => FindEntity(serial, false, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Find(Serial serial, bool returnDeleted) => FindEntity(serial, returnDeleted, false); + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static T Find(Serial serial, bool returnDeleted, bool returnPending) => FindEntity(serial, returnDeleted, returnPending); + + public static R FindEntity(Serial serial) where R : class, T => FindEntity(serial, false, false); + + public static R FindEntity(Serial serial, bool returnDeleted, bool returnPending) where R : class, T + { + switch (World.WorldState) + { + default: return null; + case WorldState.Loading: + case WorldState.Saving: + case WorldState.WritingSave: + { + if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) + { + return entity as R; + } + + if (returnPending && _pendingAdd.TryGetValue(serial, out entity) || + _entitiesBySerial.TryGetValue(serial, out entity)) + { + return entity as R; + } + + return null; + } + case WorldState.Running: + { + return _entitiesBySerial.TryGetValue(serial, out var entity) ? entity as R : null; + } + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static void OutOfMemory(string message) => throw new OutOfMemoryException(message); +} diff --git a/Projects/Server/Serialization/ISerializable.cs b/Projects/Server/Serialization/ISerializable.cs index 68de848c1..ca7b01e3a 100644 --- a/Projects/Server/Serialization/ISerializable.cs +++ b/Projects/Server/Serialization/ISerializable.cs @@ -24,8 +24,6 @@ public interface ISerializable // Should be serialized/deserialized with the index so it can be referenced by IGenericReader DateTime Created { get; set; } - // Should be serialized/deserialized with the index so it can be referenced by IGenericReader - DateTime LastSerialized { get; protected internal set; } long SavePosition { get; protected internal set; } BufferWriter SaveBuffer { get; protected internal set; } @@ -61,7 +59,6 @@ public interface ISerializable return; } - LastSerialized = Core.Now; SaveBuffer.Seek(0, SeekOrigin.Begin); Serialize(SaveBuffer); diff --git a/Projects/Server/Serialization/SerializationExtensions.cs b/Projects/Server/Serialization/SerializationExtensions.cs index 7cc63cd80..0e89cbca3 100644 --- a/Projects/Server/Serialization/SerializationExtensions.cs +++ b/Projects/Server/Serialization/SerializationExtensions.cs @@ -21,6 +21,14 @@ namespace Server; public static class SerializationExtensions { + private static readonly Dictionary> _directFinderTable = new(); + private static readonly Dictionary> _searchTable = new(); + + public static void RegisterFindEntity(this Type type, Func func) + { + _searchTable[type] = func; + } + public static T ReadEntity(this IGenericReader reader) where T : class, ISerializable { Serial serial = reader.ReadSerial(); @@ -31,21 +39,55 @@ public static class SerializationExtensions // Add to this list when creating new serializable types if (typeof(BaseGuild).IsAssignableFrom(typeT)) { - entity = World.FindGuild(serial) as T; // If we check for `entity.Deleted` here during deserialization then all guilds are deleted because // Deleted -> Disbanded -> No leader, which is the case before deserialization. - // TODO: Use a deleted flag instead, and actively check for dibanded guilds properly. + // TODO: Use a deleted flag instead, and actively check for disbanded guilds properly. + return World.FindGuild(serial) as T; } - else + + if (typeof(IEntity).IsAssignableFrom(typeT)) { - entity = World.FindEntity(serial) as T; - if (entity?.Deleted == false) + return World.FindEntity(serial, returnPending: false) as T; + } + + if (_directFinderTable.TryGetValue(typeT, out var finder)) + { + return finder(serial, false, false) as T; + } + + Type type = null; + foreach (var baseType in _searchTable.Keys) + { + if (baseType.IsAssignableFrom(typeT)) { - return entity; + type = baseType; + break; } } - return entity?.Created <= reader.LastSerialized ? entity : null; + if (type == null) + { + type = typeT; + while (true) + { + var baseType = type?.BaseType; + + // Find the parent class with ISerializable registered. To do this we break on it's parent class (or object) + // that doesn't have ISerializable implemented. + if (baseType?.GetInterface("ISerializable") == null && type?.GetInterface("ISerializable") != null) + { + break; + } + + type = baseType; + } + + throw new Exception($"No FindEntity registered for '{type.FullName}'."); + } + + finder = _searchTable[type]; + _directFinderTable[type] = finder; + return finder(serial, false, false) as T; } public static List ReadEntityList( diff --git a/Projects/Server/World/EntityPersistence.cs b/Projects/Server/World/EntityPersistence.cs index af33fe02a..e11205c94 100644 --- a/Projects/Server/World/EntityPersistence.cs +++ b/Projects/Server/World/EntityPersistence.cs @@ -48,7 +48,7 @@ public static class EntityPersistence using var idx = new BinaryFileWriter(idxPath, false, types); using var bin = new BinaryFileWriter(binPath, true, types); - idx.Write(2); // Version + idx.Write(3); // Version idx.Write(entities.Count); foreach (var e in entities.Values) { @@ -58,7 +58,6 @@ public static class EntityPersistence idx.Write(t); idx.Write(e.Serial); idx.Write(e.Created.Ticks); - idx.Write(e.LastSerialized.Ticks); idx.Write(start); e.SerializeTo(bin); @@ -152,7 +151,10 @@ public static class EntityPersistence var serial = idxReader.ReadUInt32(); var created = version == 0 ? now : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc); - var lastSerialized = version == 0 ? DateTime.MinValue : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc); + if (version is > 0 and < 3) + { + idxReader.ReadInt64(); // LastSerialized + } var pos = idxReader.ReadInt64(); var length = idxReader.ReadInt32(); @@ -168,7 +170,6 @@ public static class EntityPersistence if (ctor.Invoke(ctorArgs) is T entity) { entity.Created = created; - entity.LastSerialized = lastSerialized; entities.Add(new EntitySpan(entity, pos, length)); map[indexer] = entity; } @@ -223,7 +224,7 @@ public static class EntityPersistence var buffer = GC.AllocateUninitializedArray(entry.Length); if (br == null) { - br = new BufferReader(buffer, t.LastSerialized, serializedTypes); + br = new BufferReader(buffer, serializedTypes); } else { diff --git a/Projects/Server/World/World.cs b/Projects/Server/World/World.cs index a00acc9dc..ba51f033b 100644 --- a/Projects/Server/World/World.cs +++ b/Projects/Server/World/World.cs @@ -20,6 +20,7 @@ using System.Diagnostics; using System.IO; using System.Linq; using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; using System.Threading; using Server.Guilds; using Server.Logging; @@ -592,9 +593,10 @@ public static class World } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static IEntity FindEntity(Serial serial, bool returnDeleted = false) => FindEntity(serial, returnDeleted); + public static IEntity FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) => + FindEntity(serial, returnDeleted, returnPending); - public static T FindEntity(Serial serial, bool returnDeleted = false) where T : class, IEntity + public static T FindEntity(Serial serial, bool returnDeleted = false, bool returnPending = true) where T : class, IEntity { switch (WorldState) { @@ -603,43 +605,41 @@ public static class World case WorldState.Saving: case WorldState.WritingSave: { - if (returnDeleted && _pendingDelete.TryGetValue(serial, out var entity)) + if (returnDeleted && returnPending && _pendingDelete.TryGetValue(serial, out var entity)) { return entity as T; } - if (!_pendingAdd.TryGetValue(serial, out entity)) + if (!returnPending || !_pendingAdd.TryGetValue(serial, out entity)) { if (serial.IsItem) { if (Items.TryGetValue(serial, out var item)) { - entity = item; + return item as T; } } else // if (serial.IsMobile) { if (Mobiles.TryGetValue(serial, out var mob)) { - entity = mob; + return mob as T; } } } - return entity?.Deleted == false || returnDeleted ? entity as T : null; + return null; } case WorldState.Running: { if (serial.IsItem) { - Items.TryGetValue(serial, out var item); - return item as T; + return Items.TryGetValue(serial, out var item) ? item as T : null; } if (serial.IsMobile) { - Mobiles.TryGetValue(serial, out var mob); - return mob as T; + return Mobiles.TryGetValue(serial, out var mob) ? mob as T : null; } return default; @@ -684,11 +684,10 @@ public static class World { if (entity.Serial.IsItem) { - if (!Items.TryAdd(entity.Serial, entity as Item)) + ref var item = ref CollectionsMarshal.GetValueRefOrAddDefault(Items, entity.Serial, out bool exists); + if (exists) { - var existing = Items[entity.Serial]; - - if (existing == entity) + if (item == entity) { logger.Error( $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", @@ -703,21 +702,24 @@ public static class World $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Items but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", entity.GetType().FullName, entity.Serial, - existing.GetType().FullName, - existing.Serial, + item.GetType().FullName, + item.Serial, new StackTrace() ); } } + else + { + item = entity as Item; + } } if (entity.Serial.IsMobile) { - if (!Mobiles.TryAdd(entity.Serial, entity as Mobile)) + ref var mob = ref CollectionsMarshal.GetValueRefOrAddDefault(Mobiles, entity.Serial, out bool exists); + if (exists) { - var existing = Mobiles[entity.Serial]; - - if (existing == entity) + if (mob == entity) { logger.Error( $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", @@ -732,30 +734,33 @@ public static class World $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Mobiles but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", entity.GetType().FullName, entity.Serial, - existing.GetType().FullName, - existing.Serial, + mob.GetType().FullName, + mob.Serial, new StackTrace() ); } } + else + { + mob = entity as Mobile; + } } break; } } } - public static void AddGuild(BaseGuild guild) + public static void AddGuild(BaseGuild entity) { - if (!Guilds.TryAdd(guild.Serial, guild)) + ref var guild = ref CollectionsMarshal.GetValueRefOrAddDefault(Guilds, entity.Serial, out bool exists); + if (exists) { - var existing = Guilds[guild.Serial]; - - if (existing == guild) + if (guild == entity) { logger.Error( $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but it already exists in the collection.{Environment.NewLine}{{StackTrace}}", - guild.GetType().FullName, - guild.Serial, + entity.GetType().FullName, + entity.Serial, new StackTrace() ); } @@ -763,14 +768,18 @@ public static class World { logger.Error( $"Attempted to add '{{Entity}}' ({{Serial}}) to World.Guilds but found '{{ExistingEntity}}' ({{ExistingSerial}}).{Environment.NewLine}{{StackTrace}}", + entity.GetType().FullName, + entity.Serial, guild.GetType().FullName, guild.Serial, - existing.GetType().FullName, - existing.Serial, new StackTrace() ); } } + else + { + guild = entity; + } } public static void RemoveEntity(T entity) where T : class, IEntity diff --git a/Projects/UOContent/Accounting/Account.cs b/Projects/UOContent/Accounting/Account.cs index a077706da..c76feb35d 100644 --- a/Projects/UOContent/Accounting/Account.cs +++ b/Projects/UOContent/Accounting/Account.cs @@ -290,9 +290,6 @@ namespace Server.Accounting [CommandProperty(AccessLevel.GameMaster, readOnly: true)] public DateTime Created { get; set; } = Core.Now; - [CommandProperty(AccessLevel.GameMaster)] - DateTime ISerializable.LastSerialized { get; set; } = Core.Now; - public Serial Serial { get; set; } [AfterDeserialization(false)] diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs new file mode 100644 index 000000000..f40f03906 --- /dev/null +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBEntries.cs @@ -0,0 +1,9 @@ +namespace Server.Engines.BulkOrders; + +public class BOBEntries : GenericEntitySerialization +{ + public static void Configure() + { + Configure("BOBEntries"); + } +} diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs index 9f292ae18..44f1e727c 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs @@ -1,119 +1,83 @@ -namespace Server.Engines.BulkOrders +using ModernUO.Serialization; + +namespace Server.Engines.BulkOrders; + +[SerializationGenerator(1)] +public partial class BOBLargeEntry : BaseBOBEntry { - public class BOBLargeEntry : IBOBEntry + [SerializableField(0, setter: "private")] + private BOBLargeSubEntry[] _entries; + + public BOBLargeEntry(LargeBOD bod) { - public BOBLargeEntry(LargeBOD bod) + RequireExceptional = bod.RequireExceptional; + + DeedType = bod switch { - RequireExceptional = bod.RequireExceptional; + LargeTailorBOD => BODType.Tailor, + LargeSmithBOD => BODType.Smith, + _ => DeedType + }; - DeedType = bod switch - { - LargeTailorBOD => BODType.Tailor, - LargeSmithBOD => BODType.Smith, - _ => DeedType - }; + Material = bod.Material; + AmountMax = bod.AmountMax; - Material = bod.Material; - AmountMax = bod.AmountMax; + _entries = new BOBLargeSubEntry[bod.Entries.Length]; - Entries = new BOBLargeSubEntry[bod.Entries.Length]; + for (var i = 0; i < _entries.Length; ++i) + { + _entries[i] = new BOBLargeSubEntry(bod.Entries[i]); + } + } - for (var i = 0; i < Entries.Length; ++i) - { - Entries[i] = new BOBLargeSubEntry(bod.Entries[i]); - } + public override Item Reconstruct() + { + LargeBOD bod = DeedType switch + { + BODType.Smith => new LargeSmithBOD(AmountMax, RequireExceptional, Material, ReconstructEntries()), + BODType.Tailor => new LargeTailorBOD(AmountMax, RequireExceptional, Material, ReconstructEntries()), + _ => null + }; + + for (var i = 0; i < bod?.Entries.Length; ++i) + { + bod.Entries[i].Owner = bod; } - public BOBLargeEntry(IGenericReader reader) + return bod; + } + + private LargeBulkEntry[] ReconstructEntries() + { + var entries = new LargeBulkEntry[Entries.Length]; + + for (var i = 0; i < Entries.Length; ++i) { - var version = reader.ReadEncodedInt(); - - switch (version) - { - case 0: - { - RequireExceptional = reader.ReadBool(); - - DeedType = (BODType)reader.ReadEncodedInt(); - - Material = (BulkMaterialType)reader.ReadEncodedInt(); - AmountMax = reader.ReadEncodedInt(); - Price = reader.ReadEncodedInt(); - - Entries = new BOBLargeSubEntry[reader.ReadEncodedInt()]; - - for (var i = 0; i < Entries.Length; ++i) - { - Entries[i] = new BOBLargeSubEntry(reader); - } - - break; - } - } + entries[i] = new LargeBulkEntry( + null, + new SmallBulkEntry(Entries[i].ItemType, Entries[i].Number, Entries[i].Graphic) + ) + { Amount = Entries[i].AmountCur }; } - public BOBLargeSubEntry[] Entries { get; } + return entries; + } - public bool RequireExceptional { get; } + private void Deserialize(IGenericReader reader, int version) + { + RequireExceptional = reader.ReadBool(); - public BODType DeedType { get; } + DeedType = (BODType)reader.ReadEncodedInt(); + Material = (BulkMaterialType)reader.ReadEncodedInt(); + AmountMax = reader.ReadEncodedInt(); + Price = reader.ReadEncodedInt(); - public BulkMaterialType Material { get; } + _entries = new BOBLargeSubEntry[reader.ReadEncodedInt()]; - public int AmountMax { get; } - - public int Price { get; set; } - - public Item Reconstruct() + for (var i = 0; i < Entries.Length; ++i) { - LargeBOD bod = DeedType switch - { - BODType.Smith => new LargeSmithBOD(AmountMax, RequireExceptional, Material, ReconstructEntries()), - BODType.Tailor => new LargeTailorBOD(AmountMax, RequireExceptional, Material, ReconstructEntries()), - _ => null - }; - - for (var i = 0; i < bod?.Entries.Length; ++i) - { - bod.Entries[i].Owner = bod; - } - - return bod; - } - - private LargeBulkEntry[] ReconstructEntries() - { - var entries = new LargeBulkEntry[Entries.Length]; - - for (var i = 0; i < Entries.Length; ++i) - { - entries[i] = new LargeBulkEntry( - null, - new SmallBulkEntry(Entries[i].ItemType, Entries[i].Number, Entries[i].Graphic) - ) - { Amount = Entries[i].AmountCur }; - } - - return entries; - } - - public void Serialize(IGenericWriter writer) - { - writer.WriteEncodedInt(0); // version - - writer.Write(RequireExceptional); - - writer.WriteEncodedInt((int)DeedType); - writer.WriteEncodedInt((int)Material); - writer.WriteEncodedInt(AmountMax); - writer.WriteEncodedInt(Price); - - writer.WriteEncodedInt(Entries.Length); - - for (var i = 0; i < Entries.Length; ++i) - { - Entries[i].Serialize(writer); - } + _entries[i] = new BOBLargeSubEntry(); + _entries[i].Deserialize(reader); } } } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs index c818c5f2a..8a4a91637 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs @@ -1,58 +1,35 @@ using System; +using ModernUO.Serialization; -namespace Server.Engines.BulkOrders +namespace Server.Engines.BulkOrders; + +[SerializationGenerator(0)] +public partial class BOBLargeSubEntry { - public class BOBLargeSubEntry + [SerializableField(0, setter: "private")] + private Type _itemType; + + [EncodedInt] + [SerializableField(1, setter: "private")] + private int _amountCur; + + [EncodedInt] + [SerializableField(2, setter: "private")] + private int _number; + + [EncodedInt] + [SerializableField(3, setter: "private")] + private int _graphic; + + public BOBLargeSubEntry() { - public BOBLargeSubEntry(LargeBulkEntry lbe) - { - ItemType = lbe.Details.Type; - AmountCur = lbe.Amount; - Number = lbe.Details.Number; - Graphic = lbe.Details.Graphic; - } + } - public BOBLargeSubEntry(IGenericReader reader) - { - var version = reader.ReadEncodedInt(); - - switch (version) - { - case 0: - { - var type = reader.ReadString(); - - if (type != null) - { - ItemType = AssemblyHandler.FindTypeByFullName(type); - } - - AmountCur = reader.ReadEncodedInt(); - Number = reader.ReadEncodedInt(); - Graphic = reader.ReadEncodedInt(); - - break; - } - } - } - - public Type ItemType { get; } - - public int AmountCur { get; } - - public int Number { get; } - - public int Graphic { get; } - - public void Serialize(IGenericWriter writer) - { - writer.WriteEncodedInt(0); // version - - writer.Write(ItemType?.FullName); - - writer.WriteEncodedInt(AmountCur); - writer.WriteEncodedInt(Number); - writer.WriteEncodedInt(Graphic); - } + public BOBLargeSubEntry(LargeBulkEntry lbe) + { + _itemType = lbe.Details.Type; + _amountCur = lbe.Amount; + _number = lbe.Details.Number; + _graphic = lbe.Details.Graphic; } } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBSmallEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBSmallEntry.cs index 919cd72a6..42af535c4 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBSmallEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBSmallEntry.cs @@ -1,110 +1,76 @@ using System; +using ModernUO.Serialization; -namespace Server.Engines.BulkOrders +namespace Server.Engines.BulkOrders; + +[SerializationGenerator(1)] +public partial class BOBSmallEntry : BaseBOBEntry { - public class BOBSmallEntry : IBOBEntry + [SerializableField(0, setter: "private")] + private Type _itemType; + + [EncodedInt] + [SerializableField(1, setter: "private")] + private int _amountCur; + + [EncodedInt] + [SerializableField(2, setter: "private")] + private int _number; + + [EncodedInt] + [SerializableField(3, setter: "private")] + private int _graphic; + + public BOBSmallEntry(SmallBOD bod) { - public BOBSmallEntry(SmallBOD bod) + _itemType = bod.Type; + RequireExceptional = bod.RequireExceptional; + + if (bod is SmallTailorBOD) { - ItemType = bod.Type; - RequireExceptional = bod.RequireExceptional; - - if (bod is SmallTailorBOD) - { - DeedType = BODType.Tailor; - } - else if (bod is SmallSmithBOD) - { - DeedType = BODType.Smith; - } - - Material = bod.Material; - AmountCur = bod.AmountCur; - AmountMax = bod.AmountMax; - Number = bod.Number; - Graphic = bod.Graphic; + DeedType = BODType.Tailor; + } + else if (bod is SmallSmithBOD) + { + DeedType = BODType.Smith; } - public BOBSmallEntry(IGenericReader reader) + Material = bod.Material; + _amountCur = bod.AmountCur; + AmountMax = bod.AmountMax; + _number = bod.Number; + _graphic = bod.Graphic; + } + + public override Item Reconstruct() + { + SmallBOD bod = null; + + if (DeedType == BODType.Smith) { - var version = reader.ReadEncodedInt(); - - switch (version) - { - case 0: - { - var type = reader.ReadString(); - - if (type != null) - { - ItemType = AssemblyHandler.FindTypeByFullName(type); - } - - RequireExceptional = reader.ReadBool(); - - DeedType = (BODType)reader.ReadEncodedInt(); - - Material = (BulkMaterialType)reader.ReadEncodedInt(); - AmountCur = reader.ReadEncodedInt(); - AmountMax = reader.ReadEncodedInt(); - Number = reader.ReadEncodedInt(); - Graphic = reader.ReadEncodedInt(); - Price = reader.ReadEncodedInt(); - - break; - } - } + bod = new SmallSmithBOD(AmountCur, AmountMax, ItemType, Number, Graphic, RequireExceptional, Material); + } + else if (DeedType == BODType.Tailor) + { + bod = new SmallTailorBOD(AmountCur, AmountMax, ItemType, Number, Graphic, RequireExceptional, Material); } - public Type ItemType { get; } + return bod; + } - public int AmountCur { get; } + private void Deserialize(IGenericReader reader, int version) + { + _itemType = reader.ReadType(); - public int Number { get; } + RequireExceptional = reader.ReadBool(); - public int Graphic { get; } + DeedType = (BODType)reader.ReadEncodedInt(); - public bool RequireExceptional { get; } - - public BODType DeedType { get; } - - public BulkMaterialType Material { get; } - - public int AmountMax { get; } - - public int Price { get; set; } - - public Item Reconstruct() - { - SmallBOD bod = null; - - if (DeedType == BODType.Smith) - { - bod = new SmallSmithBOD(AmountCur, AmountMax, ItemType, Number, Graphic, RequireExceptional, Material); - } - else if (DeedType == BODType.Tailor) - { - bod = new SmallTailorBOD(AmountCur, AmountMax, ItemType, Number, Graphic, RequireExceptional, Material); - } - - return bod; - } - - public void Serialize(IGenericWriter writer) - { - writer.WriteEncodedInt(0); // version - - writer.Write(ItemType?.FullName); - - writer.Write(RequireExceptional); - - writer.WriteEncodedInt((int)DeedType); - writer.WriteEncodedInt((int)Material); - writer.WriteEncodedInt(AmountCur); - writer.WriteEncodedInt(AmountMax); - writer.WriteEncodedInt(Number); - writer.WriteEncodedInt(Graphic); - writer.WriteEncodedInt(Price); - } + Material = (BulkMaterialType)reader.ReadEncodedInt(); + AmountCur = reader.ReadEncodedInt(); + AmountMax = reader.ReadEncodedInt(); + Number = reader.ReadEncodedInt(); + Graphic = reader.ReadEncodedInt(); + Price = reader.ReadEncodedInt(); } } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs new file mode 100644 index 000000000..ace0a0227 --- /dev/null +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BaseBOBEntry.cs @@ -0,0 +1,60 @@ +using System; +using System.IO; +using ModernUO.Serialization; + +namespace Server.Engines.BulkOrders; + +[SerializationGenerator(1)] +public abstract partial class BaseBOBEntry : IBOBEntry +{ + [SerializableField(0, setter: "protected")] + private bool _requireExceptional; + + [SerializableField(1, setter: "protected")] + private BODType _deedType; + + [SerializableField(2, setter: "protected")] + private BulkMaterialType _material; + + [SerializableField(3, setter: "protected")] + private int _amountMax; + + [SerializableField(4)] + private int _price; + + public DateTime Created { get; set; } = Core.Now; + + public DateTime LastSerialized { get; set; } = Core.Now; + + public Serial Serial { get; } + + public bool Deleted { get; private set; } + + public BaseBOBEntry() + { + Serial = BOBEntries.NewEntity; + BOBEntries.AddEntity(this); + } + + public virtual void Delete() + { + Deleted = true; + BOBEntries.RemoveEntity(this); + } + + public abstract Item Reconstruct(); + + private void Deserialize(IGenericReader reader, int version) + { + if (version == 0) + { + // version 0 - This class didn't exist, so we are going to skip deserializing + // Seek back 1 byte because encoded int of 0 is 1 byte + reader.Seek(-1, SeekOrigin.Current); + } + else + { + throw new Exception("Unknown deserialization error in presource generated BOB Entries"); + } + } +} diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs index 0f662d55c..571cc28b8 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs @@ -244,11 +244,20 @@ namespace Server.Engines.BulkOrders switch (v) { case 0: - Entries.Add(new BOBLargeEntry(reader)); - break; + { + var largeEntry = new BOBLargeEntry(BOBEntries.NewEntity); + largeEntry.Deserialize(reader); + + Entries.Add(largeEntry); + break; + } case 1: - Entries.Add(new BOBSmallEntry(reader)); - break; + { + var smallEntry = new BOBSmallEntry(BOBEntries.NewEntity); + smallEntry.Deserialize(reader); + Entries.Add(smallEntry); + break; + } } } diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/IBOBEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/IBOBEntry.cs index 38847cd44..7ec9f8d14 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/IBOBEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/IBOBEntry.cs @@ -1,6 +1,6 @@ namespace Server.Engines.BulkOrders { - public interface IBOBEntry + public interface IBOBEntry : ISerializable { bool RequireExceptional { get; } BODType DeedType { get; } diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json new file mode 100644 index 000000000..ec12a5845 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json @@ -0,0 +1,16 @@ +{ + "version": 1, + "type": "Server.Engines.BulkOrders.BOBLargeEntry", + "properties": [ + { + "name": "Entries", + "type": "Server.Engines.BulkOrders.BOBLargeSubEntry[]", + "rule": "ArrayMigrationRule", + "ruleArguments": [ + "Server.Engines.BulkOrders.BOBLargeSubEntry", + "RawSerializableMigrationRule", + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeSubEntry.v0.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeSubEntry.v0.json new file mode 100644 index 000000000..642ee4f64 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeSubEntry.v0.json @@ -0,0 +1,35 @@ +{ + "version": 0, + "type": "Server.Engines.BulkOrders.BOBLargeSubEntry", + "properties": [ + { + "name": "ItemType", + "type": "System.Type", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "AmountCur", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Number", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Graphic", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBSmallEntry.v1.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBSmallEntry.v1.json new file mode 100644 index 000000000..5b7bbf48a --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBSmallEntry.v1.json @@ -0,0 +1,35 @@ +{ + "version": 1, + "type": "Server.Engines.BulkOrders.BOBSmallEntry", + "properties": [ + { + "name": "ItemType", + "type": "System.Type", + "rule": "PrimitiveTypeMigrationRule" + }, + { + "name": "AmountCur", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Number", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + }, + { + "name": "Graphic", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "EncodedInt" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BaseBOBEntry.v1.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BaseBOBEntry.v1.json new file mode 100644 index 000000000..648859262 --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BaseBOBEntry.v1.json @@ -0,0 +1,40 @@ +{ + "version": 1, + "type": "Server.Engines.BulkOrders.BaseBOBEntry", + "properties": [ + { + "name": "RequireExceptional", + "type": "bool", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "DeedType", + "type": "Server.Engines.BulkOrders.BODType", + "rule": "EnumMigrationRule" + }, + { + "name": "Material", + "type": "Server.Engines.BulkOrders.BulkMaterialType", + "rule": "EnumMigrationRule" + }, + { + "name": "AmountMax", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + }, + { + "name": "Price", + "type": "int", + "rule": "PrimitiveTypeMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} \ No newline at end of file diff --git a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs index 5a6697875..3dde1e15d 100644 --- a/Projects/UOContent/Mobiles/Healers/BaseHealer.cs +++ b/Projects/UOContent/Mobiles/Healers/BaseHealer.cs @@ -136,7 +136,7 @@ public abstract partial class BaseHealer : BaseVendor private void Deserialize(IGenericReader reader, int version) { - // NOTE: This is to fix a previous RunUO serialiation issue with this class: + // NOTE: This is to fix a previous RunUO serialization issue with this class: // This would be a breaking change if there is a derived class that is version 2 or higher // If that is the case, change the SerializationGenerator to a version higher than that before merging this change reader.Seek(-4, SeekOrigin.Current);