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>
This commit is contained in:
parent
b992c7b955
commit
8d1283ba27
46 changed files with 1407 additions and 223 deletions
|
|
@ -0,0 +1,76 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue