using System; using Xunit; namespace Server.Tests; [Collection("Sequential Server Tests")] public class AnchoredItemSerializationTests { private static byte[] SerializeItem(Item item) { var writer = new BufferWriter(new byte[256], true); item.Serialize(writer); return writer.Buffer[..(int)writer.Position]; } /// /// Item v11 stores LastMoved and DecayResetTime as anchored time: the serialized bytes /// are a function of item state only, not of when the save runs. Pre-v11 stored /// minutes-since-moved and delta time, which rewrote the bytes on every save. /// [Fact] public void ItemBytes_AreStable_AcrossSavesAtDifferentTimes() { var start = Core._now; try { var item = new Item(0x1F13); item.MoveToWorld(new Point3D(120, 100, 0), Map.Felucca); item.RestartDecay(); var first = SerializeItem(item); // A save hours later, with no state change, must produce identical bytes. Core._now = start + TimeSpan.FromHours(5); var second = SerializeItem(item); Assert.Equal(first, second); item.Delete(); } finally { Core._now = start; } } /// /// Pre-v11 LastMoved was stored at whole-minute precision relative to the save time and /// could never round-trip exactly. Anchored storage is absolute and exact. /// [Fact] public void LastMovedAndDecayReset_RoundTripExactly() { var item = new Item(0x1F13); Item restored = null; try { item.MoveToWorld(new Point3D(121, 100, 0), Map.Felucca); // Sub-minute precision that the old minutes encoding would have destroyed. var moved = Core.Now - TimeSpan.FromSeconds(90.5) - TimeSpan.FromMilliseconds(123); item.LastMoved = moved; item.RestartDecay(); var decayReset = item.DecayResetTime; Assert.NotEqual(default(DateTime), decayReset); var bytes = SerializeItem(item); // Deserialize enters the copy into the map's sectors, so it must be deleted too. restored = new Item((Serial)0x7ffff123u); restored.Deserialize(new BufferReader(bytes)); Assert.Equal(moved, restored.LastMoved); Assert.Equal(decayReset, restored.DecayResetTime); } finally { item.Delete(); restored?.Delete(); } } /// /// LastMoved is the real time of the move, so a load after downtime must not shift it. /// Shifting it rewrote every item record on the first save after every restart. /// [Fact] public void LastMoved_IsNotShiftedByTheLoadAnchor() { var item = new Item(0x1F13); var moved = Core.Now - TimeSpan.FromDays(30); item.LastMoved = moved; var restored = new Item((Serial)0x7ffff124u); restored.Deserialize(new BufferReader(SerializeItem(item)) { AnchoredTimeShift = TimeSpan.FromHours(2) }); Assert.Equal(moved, restored.LastMoved); Assert.Equal(default(DateTime), restored.DecayResetTime); item.Delete(); restored.Delete(); } /// /// Downtime still does not count toward decay: a countdown running when the save started /// resumes with the time it had left, carried by DecayResetTime. /// [Fact] public void Downtime_IsCreditedToARunningDecayCountdown() { var start = Core._now; try { var item = new Item(0x1F13); item.LastMoved = start - TimeSpan.FromMinutes(10); var deadline = item.ScheduledDecayTime; var downtime = TimeSpan.FromHours(2); Core._now = start + downtime; item.CreditDowntimeToDecay(start, downtime); Assert.Equal(start - TimeSpan.FromMinutes(10), item.LastMoved); Assert.Equal(deadline + downtime, item.ScheduledDecayTime); item.Delete(); } finally { Core._now = start; } } /// /// The common case: an item whose countdown had already run out gets no credit, so a restart /// leaves its record byte-identical. /// [Fact] public void ItemAlreadyDueAtSave_GetsNoCredit_AndKeepsItsBytes() { var start = Core._now; try { var item = new Item(0x1F13); item.LastMoved = start - TimeSpan.FromDays(3); var before = SerializeItem(item); var downtime = TimeSpan.FromMinutes(30); Core._now = start + downtime; item.CreditDowntimeToDecay(start, downtime); Assert.Equal(default(DateTime), item.DecayResetTime); Assert.Equal(before, SerializeItem(item)); item.Delete(); } finally { Core._now = start; } } /// /// Stat-gain stamps are absolute and not shifted at load either; downtime counts toward a /// cooldown that is only minutes long. /// [Fact] public void StatGainStamps_AreNotShiftedByTheLoadAnchor() { var mobile = new Mobile(World.NewMobile); mobile.DefaultMobileInit(); mobile.LastStrGain = Core.Now - TimeSpan.FromMinutes(3); mobile.LastIntGain = Core.Now - TimeSpan.FromMinutes(2); mobile.LastDexGain = Core.Now - TimeSpan.FromMinutes(1); var writer = new BufferWriter(new byte[4096], true); mobile.Serialize(writer); var restored = new Mobile(World.NewMobile); restored.Deserialize(new BufferReader(writer.Buffer[..(int)writer.Position]) { AnchoredTimeShift = TimeSpan.FromHours(2) }); Assert.Equal(mobile.LastStrGain, restored.LastStrGain); Assert.Equal(mobile.LastIntGain, restored.LastIntGain); Assert.Equal(mobile.LastDexGain, restored.LastDexGain); mobile.Delete(); restored.Delete(); } }