From 84153fba58b6f004cab5143fe28ea07e5e5ce448 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:43:44 -0700 Subject: [PATCH] fix: Fixes dirty-tracking gaps in generated content: setters, sub-object owners, BaseVendor (#2609) ## Why Delta world saves re-serialize an entity only when it has been marked dirty. An audit of the generated classes found three ways serialized state changes without a mark; this PR closes the ones that do not need a new generator package. ## What - **Custom `[SerializableProperty]` setters mark dirty.** Twelve hand-written setters assigned their backing field without `this.MarkDirty()`. `PlagueBeastLord.OpenedBy` was an auto-property carrying the attribute with no backing field at all; it is now a generated field with the same name, order and command-property exposure. - **Generated sub-objects are linked to their owner.** Without a `[DirtyTrackingEntity]` member the generator emits setters that mark nothing. Eight entity-owned sub-objects now carry the link and receive the owner through the constructor the generator calls: `BOBFilter` (owner is `IEntity`: a `PlayerMobile` or a `BulkOrderBook`), `BOBLargeSubEntry`, `PuzzleChestSolution` and `PuzzleChestSolutionAndTime`, `TalismanAttribute` (the random factories now take the talisman), `VendorItem`, `PlayerBBMessage`, `RaffleEntry`, `ShardPollOption`. Migration schemas were regenerated with the pinned tool; the only change is the value rule argument becoming `DeserializationRequiresParent`. - **BaseVendor uses the generator; restock amounts are no longer persisted.** The only state it wrote was which buy entries had grown restock amounts, packed by index into the live `SBInfos` tables. Restock is transient now and rebuilds on load; version 1 records are read and discarded through the legacy path. Every vendor subclass now serializes through a generated chain. ## Generator 4.1.0 This PR adopts SerializationGenerator 4.1.0 (modernuo/SerializationGenerator#55): SG3019/SG3020 diagnostics, `[VolatileSerializedState]`, `StopXxx()` timer helpers, and owner-constructor preference for sub-objects (so dictionary values are constructed with their owner; the 4.0.0 relink fallback is gone). `PlayerVendor.v3.json` gains `DeserializationRequiresParent` for its `VendorItem` values so the migration content struct constructs them with their vendor. SG3019 is an error under `TreatWarningsAsErrors` and generator diagnostics ignore pragmas, so the five contexts that live in whole-file player-keyed persistences (`ChampionTitle`, `ChampionTitleContext`, `MurderContext`, `VirtueContext`, `JailRecord`) now carry a `[DirtyTrackingEntity]` link to their `PlayerMobile`, which is where they will live once those blobs move onto the player record. `JailSystem.EmptyRecord` keeps a null player (`[CanBeNull]`). `ChampionTitle` needs both a context and a player constructor because the generator resolves the rule against the containing type but emits the call with the parent field; a comment in the file records this. ## Wire format Unchanged. No version bumps except `BaseVendor` 1 to 2 (which now writes nothing of its own). Schema diffs are rule-argument only (`DeserializationRequiresParent`). ## Testing Server.Tests 848 passed, UOContent.Tests 759 passed. --- .config/dotnet-tools.json | 2 +- Projects/Server/Server.csproj | 4 +- .../Engines/Bulk Orders/Books/BOBFilter.cs | 5 + .../Bulk Orders/Books/BOBLargeEntry.cs | 4 +- .../Bulk Orders/Books/BOBLargeSubEntry.cs | 10 +- .../Bulk Orders/Books/BulkOrderBook.cs | 4 +- .../Engines/CannedEvil/ChampionTitle.cs | 12 ++ .../CannedEvil/ChampionTitleContext.cs | 7 +- .../UOContent/Engines/Khaldun/PuzzleChest.cs | 26 ++-- .../UOContent/Engines/Plants/PlantItem.cs | 1 + .../Player Murder System/MurderContext.cs | 1 + .../UOContent/Engines/Spawners/BaseSpawner.cs | 1 + .../Engines/Virtues/VirtueContext.cs | 7 ++ .../UOContent/Engines/Virtues/VirtueSystem.cs | 4 +- Projects/UOContent/Items/Armor/BaseArmor.cs | 3 + .../Fillable Containers/FillableContainer.cs | 1 + Projects/UOContent/Items/Food/Beverage.cs | 1 + .../Minor Artifacts/ML/BloodwoodSpirit.cs | 2 +- .../Items/Minor Artifacts/ML/TotemOfVoid.cs | 2 +- .../Items/Misc/PlayerBulletinBoards.cs | 16 +-- .../Skill Items/Magical/Misc/RecallRune.cs | 1 + .../Musical Instruments/BaseInstrument.cs | 2 + .../Special/House Raffle/HouseRaffleStone.cs | 13 +- .../Talismans/BaseTalisman.Migrations.cs | 6 +- .../UOContent/Items/Talismans/BaseTalisman.cs | 48 ++++---- .../Items/Talismans/RandomTalisman.cs | 6 +- .../Items/Talismans/TalismanAttribute.cs | 14 ++- ...r.Engines.BulkOrders.BOBLargeEntry.v1.json | 2 +- ...r.Engines.BulkOrders.BulkOrderBook.v3.json | 2 +- ...es.CannedEvil.ChampionTitleContext.v1.json | 18 +-- .../Server.Items.BasePlayerBB.v0.json | 4 +- .../Server.Items.BaseTalisman.v1.json | 6 +- .../Server.Items.HouseRaffleStone.v4.json | 2 +- .../Server.Items.PuzzleChest.v1.json | 4 +- .../Server.Misc.ShardPoller.v1.json | 2 +- .../Server.Mobiles.BaseVendor.v2.json | 4 + .../Server.Mobiles.PlayerVendor.v3.json | 2 +- .../Server.Mobiles.PlayerVendor.v4.json | 2 +- Projects/UOContent/Misc/Loot.cs | 6 +- Projects/UOContent/Misc/ShardPoller.cs | 18 ++- .../Monsters/Misc/Melee/PlagueBeast.cs | 6 +- .../Monsters/Misc/Melee/PlagueBeastLord.cs | 6 +- Projects/UOContent/Mobiles/PlayerMobile.cs | 6 +- .../UOContent/Mobiles/Vendors/BaseVendor.cs | 111 +++--------------- .../UOContent/Mobiles/Vendors/PlayerVendor.cs | 4 +- .../UOContent/Mobiles/Vendors/VendorItem.cs | 12 +- .../Systems/JailSystem/JailRecord.cs | 7 ++ .../Systems/JailSystem/JailSystem.cs | 6 +- Projects/UOContent/UOContent.csproj | 4 +- 49 files changed, 223 insertions(+), 214 deletions(-) create mode 100644 Projects/UOContent/Migrations/Server.Mobiles.BaseVendor.v2.json diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f9f5fbe15..3e22e2e66 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "modernuoschemagenerator": { - "version": "4.0.0", + "version": "4.1.0", "commands": [ "ModernUOSchemaGenerator" ] diff --git a/Projects/Server/Server.csproj b/Projects/Server/Server.csproj index fd69557b7..ab982279f 100644 --- a/Projects/Server/Server.csproj +++ b/Projects/Server/Server.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs index f63e4f8d4..5b72a3c54 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBFilter.cs @@ -5,6 +5,11 @@ namespace Server.Engines.BulkOrders; [SerializationGenerator(2)] public partial class BOBFilter { + [DirtyTrackingEntity] + private IEntity _owner; + + public BOBFilter(IEntity owner) => _owner = owner; + [SerializableField(0)] [SaveFlag(nameof(ShouldSerializeType))] private int _type; diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs index 44f1e727c..b2d8873d7 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeEntry.cs @@ -26,7 +26,7 @@ public partial class BOBLargeEntry : BaseBOBEntry for (var i = 0; i < _entries.Length; ++i) { - _entries[i] = new BOBLargeSubEntry(bod.Entries[i]); + _entries[i] = new BOBLargeSubEntry(this, bod.Entries[i]); } } @@ -76,7 +76,7 @@ public partial class BOBLargeEntry : BaseBOBEntry for (var i = 0; i < Entries.Length; ++i) { - _entries[i] = new BOBLargeSubEntry(); + _entries[i] = new BOBLargeSubEntry(this); _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 8a4a91637..8337dc91b 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BOBLargeSubEntry.cs @@ -6,6 +6,9 @@ namespace Server.Engines.BulkOrders; [SerializationGenerator(0)] public partial class BOBLargeSubEntry { + [DirtyTrackingEntity] + private BOBLargeEntry _parent; + [SerializableField(0, setter: "private")] private Type _itemType; @@ -21,12 +24,11 @@ public partial class BOBLargeSubEntry [SerializableField(3, setter: "private")] private int _graphic; - public BOBLargeSubEntry() - { - } + public BOBLargeSubEntry(BOBLargeEntry parent) => _parent = parent; - public BOBLargeSubEntry(LargeBulkEntry lbe) + public BOBLargeSubEntry(BOBLargeEntry parent, LargeBulkEntry lbe) { + _parent = parent; _itemType = lbe.Details.Type; _amountCur = lbe.Amount; _number = lbe.Details.Number; diff --git a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs index a0c17c575..e4d788a7c 100644 --- a/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs +++ b/Projects/UOContent/Engines/Bulk Orders/Books/BulkOrderBook.cs @@ -43,7 +43,7 @@ public partial class BulkOrderBook : Item, ISecurable LootType = LootType.Blessed; _entries = []; - _filter = new BOBFilter(); + _filter = new BOBFilter(this); _level = SecureLevel.CoOwners; } @@ -224,7 +224,7 @@ public partial class BulkOrderBook : Item, ISecurable _bookName = reader.ReadString(); - _filter = new BOBFilter(); + _filter = new BOBFilter(this); _filter.Deserialize(reader); var count = reader.ReadEncodedInt(); diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs index c1970a449..dca633bb7 100644 --- a/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitle.cs @@ -1,11 +1,23 @@ using System; using ModernUO.Serialization; +using Server.Mobiles; namespace Server.Engines.CannedEvil; [SerializationGenerator(0)] public partial class ChampionTitle { + [DirtyTrackingEntity] + private PlayerMobile _player; + + public ChampionTitle(ChampionTitleContext context) : this(context?.Player) + { + } + + // The generator resolves the deserialization constructor against the owning type, but emits the + // owner's own dirty-tracking reference (the player) at the call site, so both overloads exist. + public ChampionTitle(PlayerMobile player) => _player = player; + [EncodedInt] [SerializableField(0)] private int _value; diff --git a/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs index 644e2b965..f610e485f 100644 --- a/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs +++ b/Projects/UOContent/Engines/CannedEvil/ChampionTitleContext.cs @@ -16,6 +16,7 @@ public partial class ChampionTitleContext [SerializedCommandProperty(AccessLevel.GameMaster)] private int _harrower; + [DirtyTrackingEntity] private PlayerMobile _player; public PlayerMobile Player => _player; @@ -45,7 +46,7 @@ public partial class ChampionTitleContext throw new NotImplementedException($"Cannot find ChampionSpawnType value {type}."); } - title = new ChampionTitle(); + title = new ChampionTitle(this); title.Deserialize(reader); } } @@ -290,7 +291,7 @@ public partial class ChampionTitleContext return null; } - return title ??= new ChampionTitle(); + return title ??= new ChampionTitle(this); } public void SetValue(ChampionSpawnType type, int value) @@ -313,7 +314,7 @@ public partial class ChampionTitleContext } else { - title = new ChampionTitle(); + title = new ChampionTitle(this); } title.Value = value; diff --git a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs index 53932c8f0..2da3b198b 100644 --- a/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs +++ b/Projects/UOContent/Engines/Khaldun/PuzzleChest.cs @@ -23,11 +23,19 @@ namespace Server.Items [SerializationGenerator(1)] public partial class PuzzleChestSolution { + [DirtyTrackingEntity] + private PuzzleChest _chest; + [SerializableField(0)] private PuzzleChestCylinder[] _cylinders; public const int Length = 5; + // Declared first: the generator picks the first matching constructor, and a deserialized + // solution must know its chest to mark it dirty. + public PuzzleChestSolution(PuzzleChest chest) : this() => _chest = chest; + + // Transient solutions (player guesses being edited in a gump) have no owning chest. public PuzzleChestSolution() => _cylinders = [RandomCylinder(), RandomCylinder(), RandomCylinder(), RandomCylinder(), RandomCylinder()]; @@ -42,6 +50,9 @@ namespace Server.Items solution.Cylinders.AsSpan().CopyTo(Cylinders); } + protected PuzzleChestSolution(PuzzleChest chest, PuzzleChestSolution solution) : this(solution) => + _chest = chest; + private void Deserialize(IGenericReader reader, int version) { var length = reader.ReadEncodedInt(); @@ -174,10 +185,11 @@ namespace Server.Items [SerializableField(0)] private DateTime _when; - public PuzzleChestSolutionAndTime(DateTime when, PuzzleChestSolution solution) : base(solution) => _when = when; + public PuzzleChestSolutionAndTime(PuzzleChest chest, DateTime when, PuzzleChestSolution solution) + : base(chest, solution) => _when = when; - // For serialization - public PuzzleChestSolutionAndTime() + // The generator deserializes guesses through this constructor so each one knows its chest. + public PuzzleChestSolutionAndTime(PuzzleChest chest) : base(chest) { } } @@ -214,7 +226,7 @@ namespace Server.Items private void Deserialize(IGenericReader reader, int version) { - _solution = new PuzzleChestSolution(); + _solution = new PuzzleChestSolution(this); _solution.Deserialize(reader); var length = reader.ReadEncodedInt(); @@ -238,7 +250,7 @@ namespace Server.Items for (var i = 0; i < guessCount; i++) { var m = reader.ReadEntity(); - (_guesses[m] = new PuzzleChestSolutionAndTime()).Deserialize(reader); + (_guesses[m] = new PuzzleChestSolutionAndTime(this)).Deserialize(reader); } } @@ -329,7 +341,7 @@ namespace Server.Items } else { - (_guesses ??= []).Add(m, new PuzzleChestSolutionAndTime(Core.Now, solution)); + (_guesses ??= []).Add(m, new PuzzleChestSolutionAndTime(this, Core.Now, solution)); StartCleanupTimer(); m.SendGump(new StatusGump(correctCylinders, correctColors)); @@ -525,7 +537,7 @@ namespace Server.Items } } - Solution = new PuzzleChestSolution(); + Solution = new PuzzleChestSolution(this); } private void StartCleanupTimer() diff --git a/Projects/UOContent/Engines/Plants/PlantItem.cs b/Projects/UOContent/Engines/Plants/PlantItem.cs index 5f66b1a7a..f829b0e20 100644 --- a/Projects/UOContent/Engines/Plants/PlantItem.cs +++ b/Projects/UOContent/Engines/Plants/PlantItem.cs @@ -96,6 +96,7 @@ public partial class PlantItem : Item, ISecurable var ratio = PlantSystem != null ? (double)PlantSystem.Hits / PlantSystem.MaxHits : 1.0; _plantStatus = value; + this.MarkDirty(); if (_plantStatus >= PlantStatus.DecorativePlant) { diff --git a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs index 384c91c98..9be182300 100644 --- a/Projects/UOContent/Engines/Player Murder System/MurderContext.cs +++ b/Projects/UOContent/Engines/Player Murder System/MurderContext.cs @@ -58,6 +58,7 @@ public partial class MurderContext _lastMurderTime = Core.Now; } + [DirtyTrackingEntity] public PlayerMobile _player; public PlayerMobile Player => _player; diff --git a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs index 8efc54b8c..750a08257 100644 --- a/Projects/UOContent/Engines/Spawners/BaseSpawner.cs +++ b/Projects/UOContent/Engines/Spawners/BaseSpawner.cs @@ -308,6 +308,7 @@ public abstract partial class BaseSpawner : Item, ISpawner { _walkingRange = value; InvalidateProperties(); + this.MarkDirty(); } } diff --git a/Projects/UOContent/Engines/Virtues/VirtueContext.cs b/Projects/UOContent/Engines/Virtues/VirtueContext.cs index 7524f77ed..e12d803f6 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueContext.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueContext.cs @@ -8,6 +8,13 @@ namespace Server.Engines.Virtues; [SerializationGenerator(1)] public partial class VirtueContext { + [DirtyTrackingEntity] + private PlayerMobile _player; + + public PlayerMobile Player => _player; + + public VirtueContext(PlayerMobile player) => _player = player; + private void MigrateFrom(V0Content content) { // Save-flagged values arrive as nullables; unset flags fall back to the same diff --git a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs index e57a2d386..457899b91 100644 --- a/Projects/UOContent/Engines/Virtues/VirtueSystem.cs +++ b/Projects/UOContent/Engines/Virtues/VirtueSystem.cs @@ -99,7 +99,7 @@ public class VirtueSystem : GenericPersistence for (var i = 0; i < contextCount; i++) { var player = reader.ReadEntity(); - var virtues = new VirtueContext(); + var virtues = new VirtueContext(player); virtues.Deserialize(reader); if (player != null && virtues.IsUsed()) @@ -122,7 +122,7 @@ public class VirtueSystem : GenericPersistence ref var context = ref CollectionsMarshal.GetValueRefOrAddDefault(_playerVirtues, from, out var exists); if (!exists) { - context = new VirtueContext(); + context = new VirtueContext(from); } return context; diff --git a/Projects/UOContent/Items/Armor/BaseArmor.cs b/Projects/UOContent/Items/Armor/BaseArmor.cs index f4062326d..a55400e5a 100644 --- a/Projects/UOContent/Items/Armor/BaseArmor.cs +++ b/Projects/UOContent/Items/Armor/BaseArmor.cs @@ -195,6 +195,7 @@ namespace Server.Items { UnscaleDurability(); _quality = value; + this.MarkDirty(); ScaleDurability(); } } @@ -213,6 +214,7 @@ namespace Server.Items { UnscaleDurability(); _durability = value; + this.MarkDirty(); ScaleDurability(); } } @@ -246,6 +248,7 @@ namespace Server.Items UnscaleDurability(); _resource = value; + this.MarkDirty(); if (CraftItem.RetainsColor(GetType())) { diff --git a/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs b/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs index da4d8b77f..60ca33cb2 100644 --- a/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs +++ b/Projects/UOContent/Items/Containers/Fillable Containers/FillableContainer.cs @@ -53,6 +53,7 @@ public abstract partial class FillableContainer : LockableContainer ClearContents(); _contentType = value; + this.MarkDirty(); Respawn(); } } diff --git a/Projects/UOContent/Items/Food/Beverage.cs b/Projects/UOContent/Items/Food/Beverage.cs index 85eecf17f..85e27531e 100644 --- a/Projects/UOContent/Items/Food/Beverage.cs +++ b/Projects/UOContent/Items/Food/Beverage.cs @@ -350,6 +350,7 @@ public abstract partial class BaseBeverage : Item, IHasQuantity set { _quantity = Math.Clamp(value, 0, MaxQuantity); + this.MarkDirty(); InvalidateProperties(); diff --git a/Projects/UOContent/Items/Minor Artifacts/ML/BloodwoodSpirit.cs b/Projects/UOContent/Items/Minor Artifacts/ML/BloodwoodSpirit.cs index ad854cef6..4d6a70dd5 100644 --- a/Projects/UOContent/Items/Minor Artifacts/ML/BloodwoodSpirit.cs +++ b/Projects/UOContent/Items/Minor Artifacts/ML/BloodwoodSpirit.cs @@ -13,7 +13,7 @@ public partial class BloodwoodSpirit : BaseTalisman Removal = TalismanRemoval.Damage; Blessed = GetRandomBlessed(); - Protection = GetRandomProtection(false); + Protection = GetRandomProtection(this, false); SkillBonuses.SetValues(0, SkillName.SpiritSpeak, 10.0); SkillBonuses.SetValues(1, SkillName.Necromancy, 5.0); diff --git a/Projects/UOContent/Items/Minor Artifacts/ML/TotemOfVoid.cs b/Projects/UOContent/Items/Minor Artifacts/ML/TotemOfVoid.cs index 546f906c4..885130daf 100644 --- a/Projects/UOContent/Items/Minor Artifacts/ML/TotemOfVoid.cs +++ b/Projects/UOContent/Items/Minor Artifacts/ML/TotemOfVoid.cs @@ -14,7 +14,7 @@ public partial class TotemOfVoid : BaseTalisman MaxChargeTime = 1800; Blessed = GetRandomBlessed(); - Protection = GetRandomProtection(false); + Protection = GetRandomProtection(this, false); Attributes.RegenHits = 2; Attributes.LowerManaCost = 10; diff --git a/Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs b/Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs index 0dcd418aa..1c193c32d 100644 --- a/Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs +++ b/Projects/UOContent/Items/Misc/PlayerBulletinBoards.cs @@ -74,13 +74,13 @@ public abstract partial class BasePlayerBB : Item, ISecurable if (_greeting != null) { - board.Greeting = new PlayerBBMessage(_greeting.Time, _greeting.Poster, _greeting.Message); + board.Greeting = new PlayerBBMessage(board, _greeting.Time, _greeting.Poster, _greeting.Message); } for (var i = 0; i < _messages.Count; i++) { var message = _messages[i]; - board.AddToMessages(new PlayerBBMessage(message.Time, message.Poster, message.Message)); + board.AddToMessages(new PlayerBBMessage(board, message.Time, message.Poster, message.Message)); } } @@ -176,7 +176,7 @@ public abstract partial class BasePlayerBB : Item, ISecurable if (text.Length > 0) { - var message = new PlayerBBMessage(Core.Now, from, text); + var message = new PlayerBBMessage(board, Core.Now, from, text); if (_greeting) { @@ -265,6 +265,9 @@ public abstract partial class BasePlayerBB : Item, ISecurable [SerializationGenerator(0)] public partial class PlayerBBMessage { + [DirtyTrackingEntity] + private BasePlayerBB _board; + [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private DateTime _time; @@ -277,12 +280,11 @@ public partial class PlayerBBMessage [SerializedCommandProperty(AccessLevel.GameMaster)] private string _message; - public PlayerBBMessage() - { - } + public PlayerBBMessage(BasePlayerBB board) => _board = board; - public PlayerBBMessage(DateTime time, Mobile poster, string message) + public PlayerBBMessage(BasePlayerBB board, DateTime time, Mobile poster, string message) { + _board = board; _time = time; _poster = poster; _message = message; diff --git a/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs b/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs index 14db7ac1e..1bb5a7f6e 100644 --- a/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs +++ b/Projects/UOContent/Items/Skill Items/Magical/Misc/RecallRune.cs @@ -42,6 +42,7 @@ public partial class RecallRune : Item set { _house = value; + this.MarkDirty(); CalculateHue(); InvalidateProperties(); } diff --git a/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs b/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs index f7716a4a1..5adff2ce7 100644 --- a/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs +++ b/Projects/UOContent/Items/Skill Items/Musical Instruments/BaseInstrument.cs @@ -92,6 +92,7 @@ public abstract partial class BaseInstrument : Item, ICraftable, ISlayer { UnscaleUses(); _quality = value; + this.MarkDirty(); InvalidateProperties(); ScaleUses(); } @@ -109,6 +110,7 @@ public abstract partial class BaseInstrument : Item, ICraftable, ISlayer set { _usesRemaining = value; + this.MarkDirty(); InvalidateProperties(); } } diff --git a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs index 59e2b8277..21b0e5eb2 100644 --- a/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs +++ b/Projects/UOContent/Items/Special/House Raffle/HouseRaffleStone.cs @@ -16,6 +16,9 @@ namespace Server.Items; [SerializationGenerator(0)] public partial class RaffleEntry { + [DirtyTrackingEntity] + private HouseRaffleStone _stone; + [SerializableField(0, setter: "private")] private Mobile _from; @@ -25,15 +28,17 @@ public partial class RaffleEntry [SerializableField(2, setter: "private")] private DateTime _date; - public RaffleEntry(Mobile from) + public RaffleEntry(HouseRaffleStone stone, Mobile from) { + _stone = stone; _from = from; _address = from?.NetState?.Address ?? IPAddress.None; _date = Core.Now; } - public RaffleEntry() + public RaffleEntry(HouseRaffleStone stone) { + _stone = stone; _from = null; _address = null; _date = Core.Now; @@ -454,7 +459,7 @@ public partial class HouseRaffleStone : Item if (_ticketPrice == 0 || from.Backpack?.ConsumeTotal(typeof(Gold), _ticketPrice) == true || Banker.Withdraw(from, _ticketPrice)) { - AddToEntries(new RaffleEntry(from)); + AddToEntries(new RaffleEntry(this, from)); from.SendMessage(MessageHue, "You have successfully entered the plot's raffle."); } @@ -539,7 +544,7 @@ public partial class HouseRaffleStone : Item for (var i = 0; i < entryCount; i++) { - var entry = new RaffleEntry(); + var entry = new RaffleEntry(this); entry.Deserialize(reader); if (entry.From == null) diff --git a/Projects/UOContent/Items/Talismans/BaseTalisman.Migrations.cs b/Projects/UOContent/Items/Talismans/BaseTalisman.Migrations.cs index 85d41c852..c2da33683 100644 --- a/Projects/UOContent/Items/Talismans/BaseTalisman.Migrations.cs +++ b/Projects/UOContent/Items/Talismans/BaseTalisman.Migrations.cs @@ -26,19 +26,19 @@ public partial class BaseTalisman BlessedFor = reader.ReadEntity(); } - _protection = new TalismanAttribute(); + _protection = new TalismanAttribute(this); if (GetOldSaveFlag(flags, OldSaveFlag.Protection)) { _protection.Deserialize(reader); } - _killer = new TalismanAttribute(); + _killer = new TalismanAttribute(this); if (GetOldSaveFlag(flags, OldSaveFlag.Killer)) { _killer.Deserialize(reader); } - _summoner = new TalismanAttribute(); + _summoner = new TalismanAttribute(this); if (GetOldSaveFlag(flags, OldSaveFlag.Summoner)) { _summoner.Deserialize(reader); diff --git a/Projects/UOContent/Items/Talismans/BaseTalisman.cs b/Projects/UOContent/Items/Talismans/BaseTalisman.cs index b3f0c2ed4..f7b118df6 100644 --- a/Projects/UOContent/Items/Talismans/BaseTalisman.cs +++ b/Projects/UOContent/Items/Talismans/BaseTalisman.cs @@ -155,7 +155,7 @@ public partial class BaseTalisman : Item, IAosItem public bool ShouldSerializeProtection() => !_protection.IsEmpty; - private TalismanAttribute ProtectionDefaultValue() => new(); + private TalismanAttribute ProtectionDefaultValue() => new(this); [SerializedIgnoreDupe] [InvalidateProperties] @@ -166,7 +166,7 @@ public partial class BaseTalisman : Item, IAosItem public bool ShouldSerializeKiller() => !_killer.IsEmpty; - private TalismanAttribute KillerDefaultValue() => new(); + private TalismanAttribute KillerDefaultValue() => new(this); [SerializedIgnoreDupe] [InvalidateProperties] @@ -177,7 +177,7 @@ public partial class BaseTalisman : Item, IAosItem public bool ShouldSerializeSummoner() => !_summoner.IsEmpty; - private TalismanAttribute SummonerDefaultValue() => new(); + private TalismanAttribute SummonerDefaultValue() => new(this); [InvalidateProperties] [SerializableField(5)] @@ -267,9 +267,9 @@ public partial class BaseTalisman : Item, IAosItem { Layer = Layer.Talisman; - _protection = new TalismanAttribute(); - _killer = new TalismanAttribute(); - _summoner = new TalismanAttribute(); + _protection = new TalismanAttribute(this); + _killer = new TalismanAttribute(this); + _summoner = new TalismanAttribute(this); Attributes = new AosAttributes(this); SkillBonuses = new AosSkillBonuses(this); } @@ -319,9 +319,9 @@ public partial class BaseTalisman : Item, IAosItem return; } - talisman._summoner = new TalismanAttribute(_summoner); - talisman._protection = new TalismanAttribute(_protection); - talisman._killer = new TalismanAttribute(_killer); + talisman._summoner = new TalismanAttribute(talisman, _summoner); + talisman._protection = new TalismanAttribute(talisman, _protection); + talisman._killer = new TalismanAttribute(talisman, _killer); talisman.Attributes = new AosAttributes(newItem, Attributes); talisman.SkillBonuses = new AosSkillBonuses(newItem, SkillBonuses); } @@ -660,17 +660,17 @@ public partial class BaseTalisman : Item, IAosItem public virtual void SetSummoner(Type type, TextDefinition name) { - _summoner = new TalismanAttribute(type, name); + _summoner = new TalismanAttribute(this, type, name); } public virtual void SetProtection(Type type, TextDefinition name, int amount) { - _protection = new TalismanAttribute(type, name, amount); + _protection = new TalismanAttribute(this, type, name, amount); } public virtual void SetKiller(Type type, TextDefinition name, int amount) { - _killer = new TalismanAttribute(type, name, amount); + _killer = new TalismanAttribute(this, type, name, amount); } public virtual void StartTimer() @@ -712,18 +712,18 @@ public partial class BaseTalisman : Item, IAosItem public static Type GetRandomSummonType() => _summons.RandomElement(); - public static TalismanAttribute GetRandomSummoner() + public static TalismanAttribute GetRandomSummoner(BaseTalisman owner) { if (Utility.RandomDouble() < 0.975) { - return new TalismanAttribute(); + return new TalismanAttribute(owner); } var num = Utility.Random(_summons.Length); return num > 14 - ? new TalismanAttribute(_summons[num], _summonLabels[num], 10) - : new TalismanAttribute(_summons[num], _summonLabels[num]); + ? new TalismanAttribute(owner, _summons[num], _summonLabels[num], 10) + : new TalismanAttribute(owner, _summons[num], _summonLabels[num]); } public static TalismanRemoval GetRandomRemoval() @@ -736,32 +736,32 @@ public partial class BaseTalisman : Item, IAosItem return TalismanRemoval.None; } - public static TalismanAttribute GetRandomKiller() => GetRandomKiller(true); + public static TalismanAttribute GetRandomKiller(BaseTalisman owner) => GetRandomKiller(owner, true); - public static TalismanAttribute GetRandomKiller(bool includingNone) + public static TalismanAttribute GetRandomKiller(BaseTalisman owner, bool includingNone) { if (includingNone && Utility.RandomBool()) { - return new TalismanAttribute(); + return new TalismanAttribute(owner); } var num = Utility.Random(_killers.Length); - return new TalismanAttribute(_killers[num], _killerLabels[num], Utility.RandomMinMax(10, 100)); + return new TalismanAttribute(owner, _killers[num], _killerLabels[num], Utility.RandomMinMax(10, 100)); } - public static TalismanAttribute GetRandomProtection() => GetRandomProtection(true); + public static TalismanAttribute GetRandomProtection(BaseTalisman owner) => GetRandomProtection(owner, true); - public static TalismanAttribute GetRandomProtection(bool includingNone) + public static TalismanAttribute GetRandomProtection(BaseTalisman owner, bool includingNone) { if (includingNone && Utility.RandomBool()) { - return new TalismanAttribute(); + return new TalismanAttribute(owner); } var num = Utility.Random(_killers.Length); - return new TalismanAttribute(_killers[num], _killerLabels[num], Utility.RandomMinMax(5, 60)); + return new TalismanAttribute(owner, _killers[num], _killerLabels[num], Utility.RandomMinMax(5, 60)); } public static SkillName GetRandomSkill() => _skills.RandomElement(); diff --git a/Projects/UOContent/Items/Talismans/RandomTalisman.cs b/Projects/UOContent/Items/Talismans/RandomTalisman.cs index 5235c6f20..60529c203 100644 --- a/Projects/UOContent/Items/Talismans/RandomTalisman.cs +++ b/Projects/UOContent/Items/Talismans/RandomTalisman.cs @@ -8,7 +8,7 @@ public partial class RandomTalisman : BaseTalisman [Constructible] public RandomTalisman() : base(GetRandomItemID()) { - Summoner = GetRandomSummoner(); + Summoner = GetRandomSummoner(this); if (Summoner.IsEmpty) { @@ -36,8 +36,8 @@ public partial class RandomTalisman : BaseTalisman Blessed = GetRandomBlessed(); Slayer = GetRandomSlayer(); - Protection = GetRandomProtection(); - Killer = GetRandomKiller(); + Protection = GetRandomProtection(this); + Killer = GetRandomKiller(this); Skill = GetRandomSkill(); ExceptionalBonus = GetRandomExceptional(); SuccessBonus = GetRandomSuccessful(); diff --git a/Projects/UOContent/Items/Talismans/TalismanAttribute.cs b/Projects/UOContent/Items/Talismans/TalismanAttribute.cs index e1a7ad6ec..153951081 100644 --- a/Projects/UOContent/Items/Talismans/TalismanAttribute.cs +++ b/Projects/UOContent/Items/Talismans/TalismanAttribute.cs @@ -7,6 +7,9 @@ namespace Server.Items; [SerializationGenerator(1, false)] public partial class TalismanAttribute { + [DirtyTrackingEntity] + private BaseTalisman _owner; + [SerializableField(0)] [SerializedCommandProperty(AccessLevel.GameMaster)] private Type _type; @@ -19,12 +22,12 @@ public partial class TalismanAttribute [SerializedCommandProperty(AccessLevel.GameMaster)] private int _amount; - public TalismanAttribute() : this(null, null) - { - } + public TalismanAttribute(BaseTalisman owner) => _owner = owner; - public TalismanAttribute(TalismanAttribute copy) + public TalismanAttribute(BaseTalisman owner, TalismanAttribute copy) { + _owner = owner; + if (copy != null) { _type = copy.Type; @@ -33,8 +36,9 @@ public partial class TalismanAttribute } } - public TalismanAttribute(Type type, TextDefinition name, int amount = 0) + public TalismanAttribute(BaseTalisman owner, Type type, TextDefinition name, int amount = 0) { + _owner = owner; _type = type; _name = name; _amount = amount; diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json index ec12a5845..43bc92702 100644 --- a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BOBLargeEntry.v1.json @@ -9,7 +9,7 @@ "ruleArguments": [ "Server.Engines.BulkOrders.BOBLargeSubEntry", "RawSerializableMigrationRule", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BulkOrderBook.v3.json b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BulkOrderBook.v3.json index 37899091a..74f878450 100644 --- a/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BulkOrderBook.v3.json +++ b/Projects/UOContent/Migrations/Server.Engines.BulkOrders.BulkOrderBook.v3.json @@ -28,7 +28,7 @@ "type": "Server.Engines.BulkOrders.BOBFilter", "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { diff --git a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json index af6658d06..c2811c097 100644 --- a/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json +++ b/Projects/UOContent/Migrations/Server.Engines.CannedEvil.ChampionTitleContext.v1.json @@ -16,7 +16,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -25,7 +25,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -34,7 +34,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -43,7 +43,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -52,7 +52,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -61,7 +61,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -70,7 +70,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -79,7 +79,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -88,7 +88,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Items.BasePlayerBB.v0.json b/Projects/UOContent/Migrations/Server.Items.BasePlayerBB.v0.json index 2d19ebd1a..09f2db9d1 100644 --- a/Projects/UOContent/Migrations/Server.Items.BasePlayerBB.v0.json +++ b/Projects/UOContent/Migrations/Server.Items.BasePlayerBB.v0.json @@ -20,7 +20,7 @@ "type": "Server.Items.PlayerBBMessage", "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "", + "DeserializationRequiresParent", "@CanBeNull" ] }, @@ -31,7 +31,7 @@ "ruleArguments": [ "Server.Items.PlayerBBMessage", "RawSerializableMigrationRule", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Items.BaseTalisman.v1.json b/Projects/UOContent/Migrations/Server.Items.BaseTalisman.v1.json index 958652db9..2d997d4a5 100644 --- a/Projects/UOContent/Migrations/Server.Items.BaseTalisman.v1.json +++ b/Projects/UOContent/Migrations/Server.Items.BaseTalisman.v1.json @@ -26,7 +26,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -35,7 +35,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -44,7 +44,7 @@ "usesSaveFlag": true, "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { diff --git a/Projects/UOContent/Migrations/Server.Items.HouseRaffleStone.v4.json b/Projects/UOContent/Migrations/Server.Items.HouseRaffleStone.v4.json index b2d79f227..351cb26dc 100644 --- a/Projects/UOContent/Migrations/Server.Items.HouseRaffleStone.v4.json +++ b/Projects/UOContent/Migrations/Server.Items.HouseRaffleStone.v4.json @@ -66,7 +66,7 @@ "ruleArguments": [ "Server.Items.RaffleEntry", "RawSerializableMigrationRule", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Items.PuzzleChest.v1.json b/Projects/UOContent/Migrations/Server.Items.PuzzleChest.v1.json index 508755f69..fb3360ba2 100644 --- a/Projects/UOContent/Migrations/Server.Items.PuzzleChest.v1.json +++ b/Projects/UOContent/Migrations/Server.Items.PuzzleChest.v1.json @@ -7,7 +7,7 @@ "type": "Server.Items.PuzzleChestSolution", "rule": "RawSerializableMigrationRule", "ruleArguments": [ - "" + "DeserializationRequiresParent" ] }, { @@ -32,7 +32,7 @@ "Server.Items.PuzzleChestSolutionAndTime", "RawSerializableMigrationRule", "2", - "", + "DeserializationRequiresParent", "@CanBeNull" ] } diff --git a/Projects/UOContent/Migrations/Server.Misc.ShardPoller.v1.json b/Projects/UOContent/Migrations/Server.Misc.ShardPoller.v1.json index 46345e368..e8611014a 100644 --- a/Projects/UOContent/Migrations/Server.Misc.ShardPoller.v1.json +++ b/Projects/UOContent/Migrations/Server.Misc.ShardPoller.v1.json @@ -38,7 +38,7 @@ "ruleArguments": [ "Server.Misc.ShardPollOption", "RawSerializableMigrationRule", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Mobiles.BaseVendor.v2.json b/Projects/UOContent/Migrations/Server.Mobiles.BaseVendor.v2.json new file mode 100644 index 000000000..887fd9bdf --- /dev/null +++ b/Projects/UOContent/Migrations/Server.Mobiles.BaseVendor.v2.json @@ -0,0 +1,4 @@ +{ + "version": 2, + "type": "Server.Mobiles.BaseVendor" +} \ No newline at end of file diff --git a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v3.json b/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v3.json index aee2b1da6..407b93cfc 100644 --- a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v3.json +++ b/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v3.json @@ -55,7 +55,7 @@ "Server.Mobiles.VendorItem", "RawSerializableMigrationRule", "1", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json b/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json index 6b9c9eb1c..a2c354874 100644 --- a/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json +++ b/Projects/UOContent/Migrations/Server.Mobiles.PlayerVendor.v4.json @@ -55,7 +55,7 @@ "Server.Mobiles.VendorItem", "RawSerializableMigrationRule", "1", - "" + "DeserializationRequiresParent" ] } ] diff --git a/Projects/UOContent/Misc/Loot.cs b/Projects/UOContent/Misc/Loot.cs index 2477c25b5..808364298 100644 --- a/Projects/UOContent/Misc/Loot.cs +++ b/Projects/UOContent/Misc/Loot.cs @@ -738,7 +738,7 @@ namespace Server { var talisman = new BaseTalisman(BaseTalisman.GetRandomItemID()); - talisman.Summoner = BaseTalisman.GetRandomSummoner(); + talisman.Summoner = BaseTalisman.GetRandomSummoner(talisman); if (talisman.Summoner.IsEmpty) { @@ -758,8 +758,8 @@ namespace Server talisman.Blessed = BaseTalisman.GetRandomBlessed(); talisman.Slayer = BaseTalisman.GetRandomSlayer(); - talisman.Protection = BaseTalisman.GetRandomProtection(); - talisman.Killer = BaseTalisman.GetRandomKiller(); + talisman.Protection = BaseTalisman.GetRandomProtection(talisman); + talisman.Killer = BaseTalisman.GetRandomKiller(talisman); talisman.Skill = BaseTalisman.GetRandomSkill(); talisman.ExceptionalBonus = BaseTalisman.GetRandomExceptional(); talisman.SuccessBonus = BaseTalisman.GetRandomSuccessful(); diff --git a/Projects/UOContent/Misc/ShardPoller.cs b/Projects/UOContent/Misc/ShardPoller.cs index 8516e6678..643657c1f 100644 --- a/Projects/UOContent/Misc/ShardPoller.cs +++ b/Projects/UOContent/Misc/ShardPoller.cs @@ -187,7 +187,7 @@ public partial class ShardPoller : Item for (var i = 0; i < _options.Length; ++i) { - var option = _options[i] = new ShardPollOption(); + var option = _options[i] = new ShardPollOption(this); option.Deserialize(reader); } } @@ -212,15 +212,23 @@ public partial class ShardPoller : Item [SerializationGenerator(1, false)] public partial class ShardPollOption { + [DirtyTrackingEntity] + private ShardPoller _poller; + private int _lineBreaks = -1; [SerializableField(1)] private IPAddress[] _voters; - public ShardPollOption() => _voters = []; - - public ShardPollOption(string title) + public ShardPollOption(ShardPoller poller) { + _poller = poller; + _voters = []; + } + + public ShardPollOption(ShardPoller poller, string title) + { + _poller = poller; _title = title; _voters = []; } @@ -600,7 +608,7 @@ public partial class ShardPollPrompt : Prompt if (_option == null) { - _poller.AddOption(new ShardPollOption(text)); + _poller.AddOption(new ShardPollOption(_poller, text)); } else { diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs index 8c7984360..05fc563b7 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeast.cs @@ -21,7 +21,11 @@ namespace Server.Mobiles public int DevourGoal { get => IsParagon ? _devourGoal + 25 : _devourGoal; - set => _devourGoal = value; + set + { + _devourGoal = value; + this.MarkDirty(); + } } [Constructible] diff --git a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeastLord.cs b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeastLord.cs index bbcef613e..0fd0c5bb3 100644 --- a/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeastLord.cs +++ b/Projects/UOContent/Mobiles/Monsters/Misc/Melee/PlagueBeastLord.cs @@ -52,9 +52,9 @@ namespace Server.Mobiles VirtualArmor = 50; } - [CommandProperty(AccessLevel.GameMaster)] - [SerializableProperty(3)] - public Mobile OpenedBy { get; set; } + [SerializedCommandProperty(AccessLevel.GameMaster)] + [SerializableField(3)] + private Mobile _openedBy; [CommandProperty(AccessLevel.GameMaster)] diff --git a/Projects/UOContent/Mobiles/PlayerMobile.cs b/Projects/UOContent/Mobiles/PlayerMobile.cs index 83c0e4cca..d0b1b2c7a 100644 --- a/Projects/UOContent/Mobiles/PlayerMobile.cs +++ b/Projects/UOContent/Mobiles/PlayerMobile.cs @@ -199,7 +199,7 @@ namespace Server.Mobiles VisibilityList = new List(); PermaFlags = new List(); - BOBFilter = new BOBFilter(); + BOBFilter = new BOBFilter(this); m_GameTime = TimeSpan.Zero; m_GuildRank = RankDefinition.Lowest; @@ -2958,7 +2958,7 @@ namespace Server.Mobiles case 13: // just removed m_PaidInsurance list case 12: { - BOBFilter = new BOBFilter(); + BOBFilter = new BOBFilter(this); BOBFilter.Deserialize(reader); goto case 11; } @@ -3081,7 +3081,7 @@ namespace Server.Mobiles } PermaFlags ??= new List(); - BOBFilter ??= new BOBFilter(); + BOBFilter ??= new BOBFilter(this); // Default to member if going from older version to new version (only time it should be null) m_GuildRank ??= RankDefinition.Member; diff --git a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs index 485d56c1d..135dd5ca4 100644 --- a/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/BaseVendor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using ModernUO.Serialization; using Server.Collections; using Server.ContextMenus; using Server.Engines.BulkOrders; @@ -24,7 +25,8 @@ namespace Server.Mobiles ThighBoots } - public abstract class BaseVendor : BaseCreature, IVendor + [SerializationGenerator(2, false)] + public abstract partial class BaseVendor : BaseCreature, IVendor { private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseVendor)); private const int MaxSell = 500; @@ -1297,109 +1299,28 @@ namespace Server.Mobiles Region.GetRegion()?.CheckVendorAccess(this, from) != false || Region != from.Region && from.Region.GetRegion()?.CheckVendorAccess(this, from) != false; - public override void Serialize(IGenericWriter writer) + [AfterDeserialization] + private void AfterDeserialization() { - base.Serialize(writer); - - writer.Write(1); // version - - var sbInfos = SBInfos; - - for (var i = 0; i < sbInfos?.Count; ++i) - { - var sbInfo = sbInfos[i]; - var buyInfo = sbInfo.BuyInfo; - - for (var j = 0; j < buyInfo?.Count; ++j) - { - var gbi = buyInfo[j]; - - var maxAmount = gbi.MaxAmount; - - var doubled = maxAmount switch - { - 40 => 1, - 80 => 2, - 160 => 3, - 320 => 4, - 640 => 5, - 999 => 6, - _ => 0 - }; - - if (doubled > 0) - { - writer.WriteEncodedInt(1 + j * sbInfos.Count + i); - writer.WriteEncodedInt(doubled); - } - } - } - - writer.WriteEncodedInt(0); - } - - public override void Deserialize(IGenericReader reader) - { - base.Deserialize(reader); - - var version = reader.ReadInt(); - LoadSBInfo(); - var sbInfos = SBInfos; - - switch (version) - { - case 1: - { - int index; - - while ((index = reader.ReadEncodedInt()) > 0) - { - var doubled = reader.ReadEncodedInt(); - - if (sbInfos != null) - { - index -= 1; - var sbInfoIndex = index % sbInfos.Count; - var buyInfoIndex = index / sbInfos.Count; - - if (sbInfoIndex >= 0 && sbInfoIndex < sbInfos.Count) - { - var sbInfo = sbInfos[sbInfoIndex]; - var buyInfo = sbInfo.BuyInfo; - - if (buyInfo != null && buyInfoIndex >= 0 && buyInfoIndex < buyInfo.Count) - { - var gbi = buyInfo[buyInfoIndex]; - - var amount = doubled switch - { - 1 => 40, - 2 => 80, - 3 => 160, - 4 => 320, - 5 => 640, - 6 => 999, - _ => 20 - }; - - gbi.Amount = gbi.MaxAmount = amount; - } - } - } - } - - break; - } - } - if (IsParagon) { IsParagon = false; } } + // Version 1 persisted which buy entries had grown restock amounts, packed by index into + // the live SBInfos tables. Restock is transient now: it rebuilds from SBInfos on load, so + // the pairs are read and discarded. + private void Deserialize(IGenericReader reader, int version) + { + while (reader.ReadEncodedInt() > 0) + { + reader.ReadEncodedInt(); + } + } + public override void AddCustomContextEntries(Mobile from, ref PooledRefList list) { if (from.Alive && IsActiveVendor) diff --git a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs index 9b8f5a2f3..6dc44c973 100644 --- a/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs +++ b/Projects/UOContent/Mobiles/Vendors/PlayerVendor.cs @@ -188,7 +188,7 @@ public partial class PlayerVendor : Mobile for (var i = 0; i < count; i++) { var item = reader.ReadEntity(); - var vi = new VendorItem(); + var vi = new VendorItem(this); vi.Deserialize(reader); _sellItems[item] = vi; } @@ -447,7 +447,7 @@ public partial class PlayerVendor : Mobile { RemoveVendorItem(item); - var vi = new VendorItem(item, price, description, created); + var vi = new VendorItem(this, item, price, description, created); ReplaceInSellItems(item, vi); item.InvalidateProperties(); diff --git a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs index 99d1446e6..4c1e12467 100644 --- a/Projects/UOContent/Mobiles/Vendors/VendorItem.cs +++ b/Projects/UOContent/Mobiles/Vendors/VendorItem.cs @@ -7,6 +7,9 @@ namespace Server.Mobiles; [SerializationGenerator(0, false)] public partial class VendorItem { + [DirtyTrackingEntity] + private PlayerVendor _vendor; + [SerializableField(0)] private Item _item; @@ -16,12 +19,13 @@ public partial class VendorItem [SerializableField(3)] private DateTime _created; - public VendorItem() - { - } + // The generator deserializes dictionary values through this constructor so every entry + // knows its vendor. + public VendorItem(PlayerVendor vendor) => _vendor = vendor; - public VendorItem(Item item, int price, string description, DateTime created) + public VendorItem(PlayerVendor vendor, Item item, int price, string description, DateTime created) { + _vendor = vendor; _item = item; _price = price; _description = description ?? ""; diff --git a/Projects/UOContent/Systems/JailSystem/JailRecord.cs b/Projects/UOContent/Systems/JailSystem/JailRecord.cs index c57ef8622..2cec17d55 100644 --- a/Projects/UOContent/Systems/JailSystem/JailRecord.cs +++ b/Projects/UOContent/Systems/JailSystem/JailRecord.cs @@ -1,11 +1,18 @@ using System; using ModernUO.Serialization; +using Server.Mobiles; namespace Server.Systems.JailSystem; [SerializationGenerator(0)] public partial class JailRecord { + [DirtyTrackingEntity] + [CanBeNull] + private PlayerMobile _player; + + public JailRecord(PlayerMobile player) => _player = player; + [SerializableField(0)] private int _jailCount; diff --git a/Projects/UOContent/Systems/JailSystem/JailSystem.cs b/Projects/UOContent/Systems/JailSystem/JailSystem.cs index c79c44303..8802d0487 100644 --- a/Projects/UOContent/Systems/JailSystem/JailSystem.cs +++ b/Projects/UOContent/Systems/JailSystem/JailSystem.cs @@ -42,7 +42,7 @@ public class JailSystem : GenericPersistence // Jail map, change this for custom maps public static readonly Map JailMap = Map.Felucca; - private static readonly JailRecord EmptyRecord = new(); + private static readonly JailRecord EmptyRecord = new(null); private static readonly HashSet CurrentlyBeingJailed = []; private static readonly Dictionary PlayerJailRecords = []; @@ -96,7 +96,7 @@ public class JailSystem : GenericPersistence if (!PlayerJailRecords.TryGetValue(player, out var record)) { - PlayerJailRecords[player] = record = new JailRecord(); + PlayerJailRecords[player] = record = new JailRecord(player); } record.JailCount++; @@ -409,7 +409,7 @@ public class JailSystem : GenericPersistence for (var i = 0; i < count; i++) { var player = reader.ReadEntity(); - var record = new JailRecord(); + var record = new JailRecord(player); record.Deserialize(reader); if (player != null) diff --git a/Projects/UOContent/UOContent.csproj b/Projects/UOContent/UOContent.csproj index 60f28ac6f..40919e197 100644 --- a/Projects/UOContent/UOContent.csproj +++ b/Projects/UOContent/UOContent.csproj @@ -50,8 +50,8 @@ - - + +