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>
76 lines
2.3 KiB
C#
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();
|
|
}
|
|
}
|