feat: convert timers to [DeserializeTimer]; drifting timers become anchored

The 8 drifting timers ([TimerDrift]) change wire format from delta to anchored
time, so each class bumps its serialization version with a MigrateFrom that
replays the old delta read through the migration schema. The 2 wall-clock
timers (Aquarium, FountainOfLife) keep their format via wallClock: true with
no bump. Restart methods drop their TimeSpan.MinValue sentinel checks - v4
invokes them only when a timer was actually running at save.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-22 17:46:00 -07:00
parent 46bd0ca3c0
commit 6cae6db360
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
18 changed files with 486 additions and 35 deletions

View file

@ -31,9 +31,9 @@ namespace Server.Items
private bool m_EvaluateDay;
[SerializableField(0, setter: "private")]
[DeserializeTimer(nameof(DeserializeEvaluateTimer), wallClock: true)]
private Timer _evaluateTimer;
[DeserializeTimerField(0)]
private void DeserializeEvaluateTimer(TimeSpan delay)
{
_evaluateTimer = Timer.DelayCall(delay, EvaluationInterval, Evaluate);

View file

@ -37,21 +37,22 @@ namespace Server.Items
public override double DefaultWeight => 3.0;
}
[SerializationGenerator(3, false)]
[SerializationGenerator(4, false)]
public partial class DeathRobe : Robe
{
private static readonly TimeSpan m_DefaultDecayTime = TimeSpan.FromMinutes(1.0);
[TimerDrift]
[SerializableField(0)]
[DeserializeTimer(nameof(DeserializeDecayTimer))]
private Timer _decayTimer;
[DeserializeTimerField(0)]
private void DeserializeDecayTimer(TimeSpan delay)
private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay);
private void MigrateFrom(V3Content content)
{
if (delay != TimeSpan.MinValue)
if (content.DecayTimerDelay != TimeSpan.MinValue)
{
BeginDecay(delay);
DeserializeDecayTimer(content.DecayTimerDelay);
}
}

View file

@ -3,19 +3,22 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(2, false)]
[SerializationGenerator(3, false)]
public abstract partial class FillableContainer : LockableContainer
{
[TimerDrift]
[SerializableField(1)]
[DeserializeTimer(nameof(DeserializeRespawnTimer))]
private Timer _respawnTimer;
[DeserializeTimerField(1)]
private void DeserializeRespawnTimer(TimeSpan delay)
private void DeserializeRespawnTimer(TimeSpan delay) => _respawnTimer = Timer.DelayCall(delay, Respawn);
private void MigrateFrom(V2Content content)
{
if (delay > TimeSpan.MinValue)
_contentType = content.ContentType;
if (content.RespawnTimerDelay != TimeSpan.MinValue)
{
_respawnTimer = Timer.DelayCall(delay, Respawn);
DeserializeRespawnTimer(content.RespawnTimerDelay);
}
}

View file

@ -3,14 +3,13 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(1, false)]
public partial class MarkContainer : LockableContainer
{
[TimerDrift]
[SerializableField(1, getter: "private", setter: "private")]
[DeserializeTimer(nameof(DeserializeRelockTimer))]
private InternalTimer _relockTimer;
[DeserializeTimerField(1)]
private void DeserializeRelockTimer(TimeSpan delay)
{
if (!Locked && _autoLock)
@ -19,6 +18,19 @@ public partial class MarkContainer : LockableContainer
}
}
private void MigrateFrom(V0Content content)
{
_autoLock = content.AutoLock;
_targetMap = content.TargetMap;
_target = content.Target;
_description = content.Description;
if (content.RelockTimerDelay != TimeSpan.MinValue)
{
DeserializeRelockTimer(content.RelockTimerDelay);
}
}
[SerializableField(2)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private Map _targetMap;

View file

@ -9,7 +9,7 @@ using Server.Network;
namespace Server.Items;
[SerializationGenerator(3, false)]
[SerializationGenerator(4, false)]
public partial class TreasureMapChest : LockableContainer
{
[Tidy]
@ -29,12 +29,11 @@ public partial class TreasureMapChest : LockableContainer
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _level;
[TimerDrift]
[SerializableField(4)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
[DeserializeTimer(nameof(DeserializeExpireTimer))]
private Timer _expireTimer;
[DeserializeTimerField(4)]
private void DeserializeExpireTimer(TimeSpan delay)
{
if (!_temporary)
@ -43,6 +42,20 @@ public partial class TreasureMapChest : LockableContainer
}
}
private void MigrateFrom(V3Content content)
{
_guardians = content.Guardians;
_temporary = content.Temporary;
_owner = content.Owner;
_level = content.Level;
_lifted = content.Lifted;
if (content.ExpireTimerDelay != TimeSpan.MinValue)
{
DeserializeExpireTimer(content.ExpireTimerDelay);
}
}
[Tidy]
[CanBeNull]
[SerializableField(5, setter: "private")]

View file

@ -3,7 +3,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(1, false)]
[SerializationGenerator(2, false)]
public abstract partial class BaseLight : Item
{
public static readonly bool Burnout = false;
@ -16,11 +16,10 @@ public abstract partial class BaseLight : Item
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _protected;
[TimerDrift]
[SerializableField(4, getter: "private", setter: "private")]
[DeserializeTimer(nameof(DeserializeTimer))]
private Timer _burnTimer;
[DeserializeTimerField(4)]
private void DeserializeTimer(TimeSpan delay)
{
if (_burning && _duration != TimeSpan.Zero)
@ -29,6 +28,19 @@ public abstract partial class BaseLight : Item
}
}
private void MigrateFrom(V1Content content)
{
_burntOut = content.BurntOut;
_burning = content.Burning;
_duration = content.Duration;
_protected = content.Protected;
if (content.BurnTimerDelay != TimeSpan.MinValue)
{
DeserializeTimer(content.BurnTimerDelay);
}
}
[Constructible]
public BaseLight(int itemID) : base(itemID)
{

View file

@ -86,7 +86,7 @@ public enum CorpseFlag
OwnerWasAnimatedDead = 0x00000800
}
[SerializationGenerator(17, false)]
[SerializationGenerator(18, false)]
public partial class Corpse : Container, ICarvable
{
public static readonly TimeSpan MonsterLootRightSacrifice = TimeSpan.FromMinutes(2.0);
@ -114,13 +114,37 @@ public partial class Corpse : Container, ICarvable
[SerializableField(3, getter: "private", setter: "private")]
private Dictionary<Item, Point3D> _restoreTable;
[TimerDrift]
[SerializableField(4, getter: "private", setter: "private")]
[DeserializeTimer(nameof(DeserializeDecayTimer))]
private Timer _decayTimer;
[DeserializeTimerField(4)]
private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay);
private void MigrateFrom(V17Content content)
{
_restoreEquip = content.RestoreEquip;
_flags = content.Flags;
_timeOfDeath = content.TimeOfDeath;
_restoreTable = content.RestoreTable;
_looters = content.Looters;
_killer = content.Killer;
_aggressors = content.Aggressors;
_owner = content.Owner;
_corpseName = content.CorpseName;
_accessLevel = content.AccessLevel;
_guild = content.Guild;
_equipItems = content.EquipItems;
_hairItemId = content.HairItemId;
_hairHue = content.HairHue;
_facialHairItemId = content.FacialHairItemId;
_facialHairHue = content.FacialHairHue;
if (content.DecayTimerDelay != TimeSpan.MinValue)
{
DeserializeDecayTimer(content.DecayTimerDelay);
}
}
[SerializableField(5, setter: "private")]
private HashSet<Mobile> _looters;

View file

@ -3,18 +3,25 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(2, false)]
[SerializationGenerator(3, false)]
public partial class DecayedCorpse : Container
{
private static TimeSpan _defaultDecayTime = TimeSpan.FromMinutes(7.0);
[TimerDrift]
[SerializableField(0, getter: "private", setter: "private")]
[DeserializeTimer(nameof(DeserializeDecayTimer))]
private Timer _decayTimer;
[DeserializeTimerField(0)]
private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay);
private void MigrateFrom(V2Content content)
{
if (content.DecayTimerDelay != TimeSpan.MinValue)
{
DeserializeDecayTimer(content.DecayTimerDelay);
}
}
public DecayedCorpse(string name) : base(Utility.Random(0xECA, 9))
{
Movable = false;

View file

@ -31,6 +31,7 @@ public partial class FountainOfLife : BaseAddonContainer
public const int MaxCharges = 10;
[SerializableField(1)]
[DeserializeTimer(nameof(DeserializeTimer), wallClock: true)]
private Timer _timer;
[Constructible]
@ -39,7 +40,6 @@ public partial class FountainOfLife : BaseAddonContainer
_charges = charges;
}
[DeserializeTimerField(1)]
private void DeserializeTimer(TimeSpan delay)
{
_timer = Timer.DelayCall(Utility.Max(delay, TimeSpan.Zero), RechargeTime, Recharge);

View file

@ -0,0 +1,43 @@
{
"version": 2,
"type": "Server.Items.BaseLight",
"properties": [
{
"name": "BurntOut",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Burning",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Duration",
"type": "System.TimeSpan",
"rule": "PrimitiveTypeMigrationRule"
},
{
"name": "Protected",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "BurnTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
}
]
}

View file

@ -0,0 +1,137 @@
{
"version": 18,
"type": "Server.Items.Corpse",
"properties": [
{
"name": "RestoreEquip",
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Flags",
"type": "Server.Items.CorpseFlag",
"rule": "EnumMigrationRule"
},
{
"name": "TimeOfDeath",
"type": "System.DateTime",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
"DeltaTime"
]
},
{
"name": "RestoreTable",
"type": "System.Collections.Generic.Dictionary\u003CServer.Item, Server.Point3D\u003E",
"rule": "DictionaryMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule",
"0",
"Server.Point3D",
"PrimitiveUOTypeMigrationRule",
"1",
"Point3D"
]
},
{
"name": "DecayTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
},
{
"name": "Looters",
"type": "System.Collections.Generic.HashSet\u003CServer.Mobile\u003E",
"rule": "HashSetMigrationRule",
"ruleArguments": [
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Killer",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Aggressors",
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Owner",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "CorpseName",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "AccessLevel",
"type": "Server.AccessLevel",
"rule": "EnumMigrationRule"
},
{
"name": "Guild",
"type": "Server.Guilds.Guild",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "EquipItems",
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "HairItemId",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "HairHue",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "FacialHairItemId",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "FacialHairHue",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -0,0 +1,14 @@
{
"version": 4,
"type": "Server.Items.DeathRobe",
"properties": [
{
"name": "DecayTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
}
]
}

View file

@ -0,0 +1,14 @@
{
"version": 3,
"type": "Server.Items.DecayedCorpse",
"properties": [
{
"name": "DecayTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
}
]
}

View file

@ -0,0 +1,19 @@
{
"version": 3,
"type": "Server.Items.FillableContainer",
"properties": [
{
"name": "ContentType",
"type": "Server.Items.FillableContentType",
"rule": "EnumMigrationRule"
},
{
"name": "RespawnTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
}
]
}

View file

@ -0,0 +1,46 @@
{
"version": 1,
"type": "Server.Items.MarkContainer",
"properties": [
{
"name": "AutoLock",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "RelockTimer",
"type": "Server.Items.MarkContainer.InternalTimer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
},
{
"name": "TargetMap",
"type": "Server.Map",
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Map"
]
},
{
"name": "Target",
"type": "Server.Point3D",
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"Point3D"
]
},
{
"name": "Description",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
}
]
}

View file

@ -0,0 +1,57 @@
{
"version": 4,
"type": "Server.Items.TreasureMapChest",
"properties": [
{
"name": "Guardians",
"type": "System.Collections.Generic.List\u003CServer.Mobile\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"@Tidy",
"@CanBeNull",
"Server.Mobile",
"SerializableInterfaceMigrationRule"
]
},
{
"name": "Temporary",
"type": "bool",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "Owner",
"type": "Server.Mobile",
"rule": "SerializableInterfaceMigrationRule"
},
{
"name": "Level",
"type": "int",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "ExpireTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
},
{
"name": "Lifted",
"type": "System.Collections.Generic.HashSet\u003CServer.Item\u003E",
"rule": "HashSetMigrationRule",
"ruleArguments": [
"@Tidy",
"@CanBeNull",
"Server.Item",
"SerializableInterfaceMigrationRule"
]
}
]
}

View file

@ -0,0 +1,43 @@
{
"version": 3,
"type": "Server.Mobiles.BaseEscortable",
"properties": [
{
"name": "DestinationString",
"type": "string",
"rule": "PrimitiveTypeMigrationRule",
"ruleArguments": [
""
]
},
{
"name": "DeleteTimer",
"type": "Server.Timer",
"rule": "TimerMigrationRule",
"ruleArguments": [
"@AnchoredTimer"
]
},
{
"name": "MlQuestType",
"type": "System.Type",
"rule": "PrimitiveTypeMigrationRule"
},
{
"name": "MlQuestDestinationMessage",
"type": "Server.TextDefinition",
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"TextDefinition"
]
},
{
"name": "MlQuestPaymentMessage",
"type": "Server.TextDefinition",
"rule": "PrimitiveUOTypeMigrationRule",
"ruleArguments": [
"TextDefinition"
]
}
]
}

View file

@ -17,7 +17,7 @@ using EDI = Server.Mobiles.EscortDestinationInfo;
namespace Server.Mobiles;
[SerializationGenerator(2, false)]
[SerializationGenerator(3, false)]
public partial class BaseEscortable : BaseCreature
{
private static readonly ILogger logger = LogFactory.GetLogger(typeof(BaseEscortable));
@ -158,16 +158,22 @@ public partial class BaseEscortable : BaseCreature
[SerializableField(0, setter: "private")]
private string _destinationString;
[TimerDrift]
[SerializableField(1)]
[DeserializeTimer(nameof(DeserializeDeleteTimer))]
private Timer _deleteTimer;
[DeserializeTimerField(1)]
private void DeserializeDeleteTimer(TimeSpan delay)
private void DeserializeDeleteTimer(TimeSpan delay) => Timer.DelayCall(delay, Delete);
private void MigrateFrom(V2Content content)
{
if (delay >= TimeSpan.Zero)
_destinationString = content.DestinationString;
_mlQuestType = content.MlQuestType;
_mlQuestDestinationMessage = content.MlQuestDestinationMessage;
_mlQuestPaymentMessage = content.MlQuestPaymentMessage;
if (content.DeleteTimerDelay != TimeSpan.MinValue)
{
Timer.DelayCall(delay, Delete);
DeserializeDeleteTimer(content.DeleteTimerDelay);
}
}