ModernUO/Projects/Server.Tests/Tests/Serialization/AnchoredItemSerializationTests.cs
Kamron Batman 8d1283ba27
feat: convert all delta-time serialization to anchored time
Every actively-written delta-time value now stores an absolute anchored
timestamp, shifted by downtime at load: remaining time is preserved across
restarts and idle saves become byte-stable.

- Item v11: LastMoved (was whole-minute delta, rewritten every save) and
  DecayResetTime; Mobile v38: the three stat-gain stamps; BaseCreature v21:
  SummonEnd; plus VendorInventory, StealableArtifacts, and ML quest objective
  timers in their own version gates.
- 17 code-generated classes swap [DeltaDateTime] for [AnchoredDateTime] with a
  version bump and MigrateFrom each (generated from their current migration
  schemas, so old saves replay the delta reads).
- GenericPersistence payloads carry no anchor of their own, so they inherit
  the save-wide shift: entity indexes always load first, and every file in a
  save shares one SaveStartTime - World.LoadTimeShift is stamped from the idx
  v5 header and applied to generic persistence readers. This is what makes
  VirtueContext and StealableArtifacts convertible at all.
- Legacy read sites (old-version fallbacks and migration replays) keep
  ReadDeltaTime: they decode old bytes and must never change.

Acceptance tests: item serialization is byte-identical across saves hours
apart, and LastMoved/DecayResetTime round-trip exactly at sub-minute
precision (impossible under the old minutes encoding).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-22 19:19:42 -07:00

76 lines
2.3 KiB
C#

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];
}
/// <summary>
/// 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.
/// </summary>
[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;
}
}
/// <summary>
/// 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.
/// </summary>
[Fact]
public void LastMovedAndDecayReset_RoundTripExactly()
{
var item = new Item(0x1F13);
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);
var restored = new Item((Serial)0x7ffff123u);
restored.Deserialize(new BufferReader(bytes));
Assert.Equal(moved, restored.LastMoved);
Assert.Equal(decayReset, restored.DecayResetTime);
item.Delete();
}
}