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.
This commit is contained in:
parent
a2c232f4a8
commit
84153fba58
49 changed files with 223 additions and 214 deletions
|
|
@ -21,7 +21,11 @@ namespace Server.Mobiles
|
|||
public int DevourGoal
|
||||
{
|
||||
get => IsParagon ? _devourGoal + 25 : _devourGoal;
|
||||
set => _devourGoal = value;
|
||||
set
|
||||
{
|
||||
_devourGoal = value;
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
|
||||
[Constructible]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ namespace Server.Mobiles
|
|||
VisibilityList = new List<Mobile>();
|
||||
PermaFlags = new List<Mobile>();
|
||||
|
||||
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<Mobile>();
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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<GuardedRegion>()?.CheckVendorAccess(this, from) != false ||
|
||||
Region != from.Region && from.Region.GetRegion<GuardedRegion>()?.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<ContextMenuEntry> list)
|
||||
{
|
||||
if (from.Alive && IsActiveVendor)
|
||||
|
|
|
|||
|
|
@ -188,7 +188,7 @@ public partial class PlayerVendor : Mobile
|
|||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = reader.ReadEntity<Item>();
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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 ?? "";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue