Follow-up to #2729. ## Problem At load, `ReadAnchoredTime` shifted **every** anchored value forward by the downtime, including values that were already in the past when the save started. That caused two problems. **An expired deadline came back.** A deadline that passed 5 minutes before a 30-minute outage loaded 25 minutes in the future. For example: - `Sheep.NextWoolTime`: a sheep that was ready to shear stopped being ready. - `PlayerVendor.NextPayTime`: an overdue payment was pushed back. - `BaseCreature.SummonEnd`, the spell fields' `End`, `TransientItem.Expiration`, and the ChampionSpawn times could move the same way. **Records were rewritten for nothing.** The first save after every restart rewrote: - stamps of things that had already happened: VirtueContext's `LastHonorUse`, `LastSacrificeGain`, the four `Last*Loss` stamps, and `Corpse.TimeOfDeath` - every deadline that had expired long ago. ## Change - **`IGenericReader.AnchoredTimeOrigin`** is the loaded save's start time. - `GenericEntityPersistence` and `GenericPersistence` set it from `World.LoadedSaveStartTime`. - `BufferReader`, `UnmanagedDataReader` and `BinaryFileReader` expose it. - **`ReadAnchoredTime` returns any value at or before the origin exactly as saved.** - A deadline still running at save start is shifted as before and keeps its remaining time. - Readers without an origin keep the old behaviour. - **This covers every anchored field, including generated ones.** Generated `[AnchoredDateTime]` fields call `reader.ReadAnchoredTime()`, so the one change covers all 26 generated fields as well as the manual reads. `[DeserializeTimer]` only restores timers that were running at save time, so it is unaffected. - **The `[AnchoredDateTime]` section of `dev-docs/serialization.md` and the `WriteAnchoredTime` doc comment describe the new rule.** ## Behaviour change Downtime now counts toward windows measured from a past stamp: | Window | Length | |---|---| | Honor use | 5 minutes | | Sacrifice gain | 1 day | | Virtue atrophy | 7 days | | Instanced corpse looting | 3 minutes | A 30-minute outage makes a 7-day atrophy fire about 0.3% sooner. No format change. ## Tests New tests in `AnchoredTimeTests`: - A deadline running at save start keeps its remaining time. - A deadline that passed before save start stays passed. - A past stamp is not shifted. - A value exactly at save start is not shifted. `Server.Tests`: 845 passed. `UOContent.Tests`: 1140 passed.
200 lines
6.3 KiB
C#
200 lines
6.3 KiB
C#
/*************************************************************************
|
|
* ModernUO *
|
|
* Copyright 2019-2026 - ModernUO Development Team *
|
|
* Email: hi@modernuo.com *
|
|
* File: IGenericReader.cs *
|
|
* *
|
|
* This program is free software: you can redistribute it and/or modify *
|
|
* it under the terms of the GNU General Public License as published by *
|
|
* the Free Software Foundation, either version 3 of the License, or *
|
|
* (at your option) any later version. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
*************************************************************************/
|
|
|
|
using System;
|
|
using System.Buffers;
|
|
using System.Collections;
|
|
using System.IO;
|
|
using System.Net;
|
|
|
|
namespace Server;
|
|
|
|
public interface IGenericReader
|
|
{
|
|
string ReadString(bool intern = false);
|
|
public string ReadStringRaw(bool intern = false);
|
|
|
|
long ReadLong();
|
|
ulong ReadULong();
|
|
int ReadInt();
|
|
uint ReadUInt();
|
|
short ReadShort();
|
|
ushort ReadUShort();
|
|
double ReadDouble();
|
|
float ReadFloat();
|
|
byte ReadByte();
|
|
sbyte ReadSByte();
|
|
bool ReadBool();
|
|
Serial ReadSerial();
|
|
Type ReadType();
|
|
|
|
DateTime ReadDateTime() => new(ReadLong(), DateTimeKind.Utc);
|
|
TimeSpan ReadTimeSpan() => new(ReadLong());
|
|
|
|
/// <summary>
|
|
/// Decodes a legacy delta-time value. Only for reading old-version payloads (version
|
|
/// fallbacks and migration replays) — current formats store anchored time and read it
|
|
/// with <see cref="ReadAnchoredTime" />. <see cref="IGenericWriter.WriteDeltaTime" /> is
|
|
/// obsolete: no current-version format may write delta time.
|
|
/// </summary>
|
|
DateTime ReadDeltaTime()
|
|
{
|
|
return ReadLong() switch
|
|
{
|
|
long.MinValue => DateTime.MinValue,
|
|
long.MaxValue => DateTime.MaxValue,
|
|
var delta => new DateTime(delta + DateTime.UtcNow.Ticks, DateTimeKind.Utc)
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Elapsed time between the loaded save starting and this load, applied by
|
|
/// <see cref="ReadAnchoredTime" />. Zero when the source carries no anchor.
|
|
/// </summary>
|
|
TimeSpan AnchoredTimeShift => TimeSpan.Zero;
|
|
|
|
/// <summary>
|
|
/// UTC start of the loaded save. <see cref="ReadAnchoredTime" /> leaves a value at or before
|
|
/// it unshifted. Default when unknown, which shifts every value.
|
|
/// </summary>
|
|
DateTime AnchoredTimeOrigin => default;
|
|
|
|
/// <summary>
|
|
/// Reads an anchored timestamp. A deadline still in the future when the save started is
|
|
/// shifted by <see cref="AnchoredTimeShift" />, so it keeps the time it had left. A value at or
|
|
/// before the save start is returned as saved: an expired deadline stays expired instead of
|
|
/// coming back, and a record of something that already happened (a last-use or last-loss stamp)
|
|
/// keeps its bytes across restarts, with downtime counting toward its window.
|
|
/// </summary>
|
|
DateTime ReadAnchoredTime()
|
|
{
|
|
var value = ReadDateTime();
|
|
|
|
if (value == DateTime.MinValue || value == DateTime.MaxValue)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
var shift = AnchoredTimeShift;
|
|
if (shift == TimeSpan.Zero || value <= AnchoredTimeOrigin)
|
|
{
|
|
return value;
|
|
}
|
|
|
|
var ticks = value.Ticks + shift.Ticks;
|
|
|
|
if (ticks >= DateTime.MaxValue.Ticks)
|
|
{
|
|
return DateTime.MaxValue;
|
|
}
|
|
|
|
return ticks <= 0 ? DateTime.MinValue : new DateTime(ticks, DateTimeKind.Utc);
|
|
}
|
|
decimal ReadDecimal() => new([ReadInt(), ReadInt(), ReadInt(), ReadInt()]);
|
|
int ReadEncodedInt()
|
|
{
|
|
int v = 0, shift = 0;
|
|
byte b;
|
|
|
|
do
|
|
{
|
|
b = ReadByte();
|
|
v |= (b & 0x7F) << shift;
|
|
shift += 7;
|
|
}
|
|
while (b >= 0x80);
|
|
|
|
return v;
|
|
}
|
|
IPAddress ReadIPAddress()
|
|
{
|
|
var length = ReadByte();
|
|
// Either 2 ushorts, or 8 ushorts
|
|
Span<byte> integer = stackalloc byte[length];
|
|
Read(integer);
|
|
return Utility.Intern(new IPAddress(integer));
|
|
}
|
|
Point3D ReadPoint3D() => new(ReadInt(), ReadInt(), ReadInt());
|
|
Point2D ReadPoint2D() => new(ReadInt(), ReadInt());
|
|
Rectangle2D ReadRect2D() => new(ReadPoint2D(), ReadPoint2D());
|
|
Rectangle3D ReadRect3D() => new(ReadPoint3D(), ReadPoint3D());
|
|
Map ReadMap() => Map.Maps[ReadByte()];
|
|
Race ReadRace() => Race.Races[ReadByte()];
|
|
int Read(Span<byte> buffer);
|
|
unsafe T ReadEnum<T>() where T : unmanaged, Enum
|
|
{
|
|
switch (sizeof(T))
|
|
{
|
|
case 1:
|
|
{
|
|
var num = ReadByte();
|
|
return *(T*)#
|
|
}
|
|
case 2:
|
|
{
|
|
var num = ReadShort();
|
|
return *(T*)#
|
|
}
|
|
case 4:
|
|
{
|
|
var num = ReadEncodedInt();
|
|
return *(T*)#
|
|
}
|
|
case 8:
|
|
{
|
|
var num = ReadLong();
|
|
return *(T*)#
|
|
}
|
|
}
|
|
|
|
return default;
|
|
}
|
|
Guid ReadGuid()
|
|
{
|
|
Span<byte> bytes = stackalloc byte[16];
|
|
Read(bytes);
|
|
return new Guid(bytes);
|
|
}
|
|
|
|
public BitArray ReadBitArray()
|
|
{
|
|
var bitLength = ReadEncodedInt();
|
|
var byteLength = (bitLength + 7) / 8;
|
|
|
|
var buffer = ArrayPool<byte>.Shared.Rent(byteLength);
|
|
try
|
|
{
|
|
Read(buffer.AsSpan(0, byteLength));
|
|
return new BitArray(buffer) { Length = bitLength };
|
|
}
|
|
finally
|
|
{
|
|
ArrayPool<byte>.Shared.Return(buffer);
|
|
}
|
|
}
|
|
|
|
TextDefinition ReadTextDefinition()
|
|
{
|
|
return ReadEncodedInt() switch
|
|
{
|
|
0 => TextDefinition.Empty,
|
|
1 => ReadEncodedInt(),
|
|
2 => ReadString(),
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
long Seek(long offset, SeekOrigin origin);
|
|
}
|