fix: Adds Created, LastSerialized, and BeforeSerialize for all entities. (#775)
* Adds `BeforeSerialized` for entities to handle cleanup. * Adds `Created` and `LastSerialized` fields to all entities. While this is a big bloat, this will be necessary for identifying dangling references to other invalid entities. * Changes formula for determining a valid reference to be _not null, not deleted, and reference's created date must be at or before the entities last serialized date_. * Adds versioning to idx file and serializes `Created` and `LastSerialized`. * Fixes Save Stats and also disables it by default.
This commit is contained in:
parent
3f6a87483b
commit
fa5eafdaff
23 changed files with 325 additions and 88 deletions
|
|
@ -63,13 +63,21 @@ namespace Server.Guilds
|
|||
[CommandProperty(AccessLevel.Counselor)]
|
||||
public Serial Serial { get; }
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
||||
public DateTime Created { get; set; } = Core.Now;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
|
||||
|
||||
long ISerializable.SavePosition { get; set; } = -1;
|
||||
|
||||
BufferWriter ISerializable.SaveBuffer { get; set; }
|
||||
|
||||
public int TypeRef { get; private set; }
|
||||
|
||||
public abstract void BeforeSerialize();
|
||||
public abstract void Serialize(IGenericWriter writer);
|
||||
|
||||
public abstract void Deserialize(IGenericReader reader);
|
||||
public abstract void OnDelete(Mobile mob);
|
||||
|
||||
|
|
|
|||
|
|
@ -47,6 +47,10 @@ namespace Server
|
|||
{
|
||||
}
|
||||
|
||||
DateTime ISerializable.Created { get; set; } = Core.Now;
|
||||
|
||||
DateTime ISerializable.LastSerialized { get; set; } = DateTime.MaxValue;
|
||||
|
||||
long ISerializable.SavePosition { get; set; } = -1;
|
||||
|
||||
BufferWriter ISerializable.SaveBuffer { get; set; }
|
||||
|
|
@ -103,6 +107,10 @@ namespace Server
|
|||
&& p.Y >= Location.m_Y - range
|
||||
&& p.Y <= Location.m_Y + range;
|
||||
|
||||
public void BeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
public void Deserialize(IGenericReader reader)
|
||||
{
|
||||
// Should not actually be saved
|
||||
|
|
|
|||
|
|
@ -771,6 +771,12 @@ namespace Server
|
|||
AddNameProperties(list);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
||||
public DateTime Created { get; set; } = Core.Now;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
|
||||
|
||||
long ISerializable.SavePosition { get; set; } = -1;
|
||||
|
||||
BufferWriter ISerializable.SaveBuffer { get; set; }
|
||||
|
|
@ -2571,6 +2577,10 @@ namespace Server
|
|||
}
|
||||
}
|
||||
|
||||
public virtual void BeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Deserialize(IGenericReader reader)
|
||||
{
|
||||
var version = reader.ReadInt();
|
||||
|
|
|
|||
|
|
@ -1147,9 +1147,6 @@ namespace Server
|
|||
|
||||
public virtual bool ShouldCheckStatTimers => true;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public DateTime CreationTime { get; private set; }
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
public int LightLevel
|
||||
{
|
||||
|
|
@ -2472,6 +2469,12 @@ namespace Server
|
|||
AddNameProperties(list);
|
||||
}
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster, readOnly: true)]
|
||||
public DateTime Created { get; set; } = Core.Now;
|
||||
|
||||
[CommandProperty(AccessLevel.GameMaster)]
|
||||
DateTime ISerializable.LastSerialized { get; set; } = Core.Now;
|
||||
|
||||
long ISerializable.SavePosition { get; set; } = -1;
|
||||
|
||||
BufferWriter ISerializable.SaveBuffer { get; set; }
|
||||
|
|
@ -2483,7 +2486,7 @@ namespace Server
|
|||
|
||||
public virtual void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.Write(32); // version
|
||||
writer.Write(33); // version
|
||||
|
||||
writer.WriteDeltaTime(LastStrGain);
|
||||
writer.WriteDeltaTime(LastIntGain);
|
||||
|
|
@ -2519,7 +2522,7 @@ namespace Server
|
|||
|
||||
writer.Write(Corpse);
|
||||
|
||||
writer.Write(CreationTime);
|
||||
// writer.Write(CreationTime);
|
||||
|
||||
Stabled.Tidy();
|
||||
writer.Write(Stabled);
|
||||
|
|
@ -6167,12 +6170,21 @@ namespace Server
|
|||
{
|
||||
}
|
||||
|
||||
public virtual void BeforeSerialize()
|
||||
{
|
||||
}
|
||||
|
||||
public virtual void Deserialize(IGenericReader reader)
|
||||
{
|
||||
var version = reader.ReadInt();
|
||||
|
||||
switch (version)
|
||||
{
|
||||
case 33:
|
||||
{
|
||||
// Removed created
|
||||
goto case 32;
|
||||
}
|
||||
case 32:
|
||||
{
|
||||
// Removed StuckMenu
|
||||
|
|
@ -6232,7 +6244,10 @@ namespace Server
|
|||
}
|
||||
case 23:
|
||||
{
|
||||
CreationTime = reader.ReadDateTime();
|
||||
if (version < 33)
|
||||
{
|
||||
Created = reader.ReadDateTime();
|
||||
}
|
||||
|
||||
goto case 22;
|
||||
}
|
||||
|
|
@ -7818,7 +7833,6 @@ namespace Server
|
|||
DamageEntries = new List<DamageEntry>();
|
||||
|
||||
NextSkillTime = Core.TickCount;
|
||||
CreationTime = Core.Now;
|
||||
}
|
||||
|
||||
public virtual void Delta(MobileDelta flag)
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ namespace Server
|
|||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Close() => _reader.Close();
|
||||
|
||||
public DateTime LastSerialized { get; init; }
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public string ReadString(bool intern = false)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ namespace Server
|
|||
|
||||
public override long Position => _position + Index;
|
||||
|
||||
|
||||
protected override int BufferSize => 81920;
|
||||
|
||||
public override void Flush()
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ namespace Server
|
|||
_encoding = encoding ?? TextEncoding.UTF8;
|
||||
}
|
||||
|
||||
public BufferReader(byte[] buffer, DateTime LastSerialized) : this(buffer) => LastSerialized = LastSerialized;
|
||||
|
||||
public void Reset(byte[] newBuffer, out byte[] oldBuffer)
|
||||
{
|
||||
oldBuffer = _buffer;
|
||||
|
|
@ -45,6 +47,8 @@ namespace Server
|
|||
}
|
||||
|
||||
// Compatible with BinaryReader.ReadString()
|
||||
public DateTime LastSerialized { get; init; } = DateTime.MinValue;
|
||||
|
||||
public string ReadString(bool intern = false)
|
||||
{
|
||||
if (!ReadBool())
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ namespace Server
|
|||
{
|
||||
public interface IGenericReader
|
||||
{
|
||||
// Used to determine valid Entity deserialization
|
||||
DateTime LastSerialized { get; init; }
|
||||
|
||||
string ReadString(bool intern = false);
|
||||
long ReadLong();
|
||||
ulong ReadULong();
|
||||
|
|
|
|||
|
|
@ -39,14 +39,13 @@ namespace Server
|
|||
|
||||
void Write(DateTime value)
|
||||
{
|
||||
var ticks = (value.Kind switch
|
||||
// If DateTimeKind is Unspecified, we can't assume it needs to be converted.
|
||||
if (value.Kind == DateTimeKind.Local)
|
||||
{
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
DateTimeKind.Unspecified => value.ToLocalTime().ToUniversalTime(),
|
||||
_ => value
|
||||
}).Ticks;
|
||||
value = value.ToUniversalTime();
|
||||
}
|
||||
|
||||
Write(ticks);
|
||||
Write(value.Ticks);
|
||||
}
|
||||
void WriteDeltaTime(DateTime value)
|
||||
{
|
||||
|
|
@ -59,17 +58,16 @@ namespace Server
|
|||
if (value == DateTime.MaxValue)
|
||||
{
|
||||
Write(long.MaxValue);
|
||||
return;
|
||||
}
|
||||
|
||||
var ticks = (value.Kind switch
|
||||
if (value.Kind == DateTimeKind.Local)
|
||||
{
|
||||
DateTimeKind.Local => value.ToUniversalTime(),
|
||||
DateTimeKind.Unspecified => value.ToLocalTime().ToUniversalTime(),
|
||||
_ => value
|
||||
}).Ticks;
|
||||
value = value.ToUniversalTime();
|
||||
}
|
||||
|
||||
// Technically supports negative deltas for times in the past
|
||||
Write(ticks - DateTime.UtcNow.Ticks);
|
||||
Write(value.Ticks - DateTime.UtcNow.Ticks);
|
||||
}
|
||||
void Write(IPAddress value)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -20,10 +20,20 @@ namespace Server
|
|||
{
|
||||
public interface ISerializable
|
||||
{
|
||||
// Should be serialized/deserialized with the index so it can be referenced by IGenericReader
|
||||
DateTime Created { get; set; }
|
||||
|
||||
// Should be serialized/deserialized with the index so it can be referenced by IGenericReader
|
||||
DateTime LastSerialized { get; protected internal set; }
|
||||
long SavePosition { get; protected internal set; }
|
||||
BufferWriter SaveBuffer { get; protected internal set; }
|
||||
|
||||
int TypeRef { get; }
|
||||
Serial Serial { get; }
|
||||
|
||||
// Executed on every entity, before it's serialized.
|
||||
// For example, this is used to clean up weak references and mark them dirty.
|
||||
void BeforeSerialize();
|
||||
void Deserialize(IGenericReader reader);
|
||||
void Serialize(IGenericWriter writer);
|
||||
void Delete();
|
||||
|
|
@ -48,6 +58,8 @@ namespace Server
|
|||
{
|
||||
SaveBuffer ??= new BufferWriter(true);
|
||||
|
||||
BeforeSerialize();
|
||||
|
||||
// Clean, don't bother serializing
|
||||
if (SavePosition > -1)
|
||||
{
|
||||
|
|
@ -55,6 +67,7 @@ namespace Server
|
|||
return;
|
||||
}
|
||||
|
||||
LastSerialized = Core.Now;
|
||||
SaveBuffer.Seek(0, SeekOrigin.Begin);
|
||||
Serialize(SaveBuffer);
|
||||
|
||||
|
|
@ -64,7 +77,7 @@ namespace Server
|
|||
}
|
||||
else
|
||||
{
|
||||
SavePosition = -1;
|
||||
this.MarkDirty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ namespace Server
|
|||
{
|
||||
public static class EntityPersistence
|
||||
{
|
||||
private const int _idxVersion = 1;
|
||||
|
||||
public static void WriteEntities<I, T>(
|
||||
IIndexInfo<I> indexInfo,
|
||||
Dictionary<I, T> entities,
|
||||
|
|
@ -48,6 +50,7 @@ namespace Server
|
|||
using var tdb = new BinaryFileWriter(tdbPath, false);
|
||||
using var bin = new BinaryFileWriter(binPath, true);
|
||||
|
||||
idx.Write(1); // Version
|
||||
idx.Write(entities.Count);
|
||||
foreach (var e in entities.Values)
|
||||
{
|
||||
|
|
@ -55,6 +58,8 @@ namespace Server
|
|||
|
||||
idx.Write(e.TypeRef);
|
||||
idx.Write(e.Serial);
|
||||
idx.Write(e.Created.Ticks);
|
||||
idx.Write(e.LastSerialized.Ticks);
|
||||
idx.Write(start);
|
||||
|
||||
e.SerializeTo(bin);
|
||||
|
|
@ -110,12 +115,28 @@ namespace Server
|
|||
|
||||
List<Tuple<ConstructorInfo, string>> types = ReadTypes<I>(tdbReader);
|
||||
|
||||
var count = idxReader.ReadInt32();
|
||||
int count;
|
||||
var version = idxReader.ReadInt32();
|
||||
|
||||
// Handle non-versioned (version 0).
|
||||
if (version > _idxVersion || idx.Length - 4 - version * 20 == 0)
|
||||
{
|
||||
count = version;
|
||||
version = 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
count = idxReader.ReadInt32();
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
for (int i = 0; i < count; ++i)
|
||||
{
|
||||
var typeID = idxReader.ReadInt32();
|
||||
var number = idxReader.ReadUInt32();
|
||||
var serial = idxReader.ReadUInt32();
|
||||
var created = version == 0 ? now : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc);
|
||||
var lastSerialized = version == 0 ? DateTime.MinValue : new DateTime(idxReader.ReadInt64(), DateTimeKind.Utc);
|
||||
var pos = idxReader.ReadInt64();
|
||||
var length = idxReader.ReadInt32();
|
||||
|
||||
|
|
@ -127,12 +148,14 @@ namespace Server
|
|||
}
|
||||
|
||||
ConstructorInfo ctor = objs.Item1;
|
||||
I indexer = indexInfo.CreateIndex(number);
|
||||
I indexer = indexInfo.CreateIndex(serial);
|
||||
|
||||
ctorArgs[0] = indexer;
|
||||
|
||||
if (ctor.Invoke(ctorArgs) is T t)
|
||||
{
|
||||
t.Created = created;
|
||||
t.LastSerialized = lastSerialized;
|
||||
entities.Add(new EntityIndex<T>(t, typeID, pos, length));
|
||||
map[indexer] = t;
|
||||
}
|
||||
|
|
@ -180,7 +203,7 @@ namespace Server
|
|||
var buffer = GC.AllocateUninitializedArray<byte>(entry.Length);
|
||||
if (br == null)
|
||||
{
|
||||
br = new BufferReader(buffer);
|
||||
br = new BufferReader(buffer, t.LastSerialized);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
|
|||
|
|
@ -47,6 +47,7 @@ namespace Server
|
|||
|
||||
private static string _tempSavePath; // Path to the temporary folder for the save
|
||||
private static string _savePath; // Path to "Saves" folder
|
||||
private static bool _enableSaveStats;
|
||||
|
||||
public const bool DirtyTrackingEnabled = false;
|
||||
public const uint ItemOffset = 0x40000000;
|
||||
|
|
@ -144,6 +145,7 @@ namespace Server
|
|||
_tempSavePath = Path.Combine(Core.BaseDirectory, tempSavePath);
|
||||
var savePath = ServerConfiguration.GetOrUpdateSetting("world.savePath", "Saves");
|
||||
_savePath = Path.Combine(Core.BaseDirectory, savePath);
|
||||
_enableSaveStats = ServerConfiguration.GetOrUpdateSetting("world.enableSaveStats", false);
|
||||
|
||||
// Mobiles & Items
|
||||
Persistence.Register("Mobiles & Items", SaveEntities, WriteEntities, LoadEntities, 1);
|
||||
|
|
@ -341,7 +343,10 @@ namespace Server
|
|||
int count = 0;
|
||||
|
||||
var timestamp = Utility.GetTimeStamp();
|
||||
using var op = new StreamWriter("Logs/Saves/Save-Stats-{0}.log", true);
|
||||
var saveStatsPath = Path.Combine(Core.BaseDirectory, $"Logs/Saves/Save-Stats-{timestamp}.log");
|
||||
AssemblyHandler.EnsureDirectory(saveStatsPath);
|
||||
|
||||
using var op = new StreamWriter(saveStatsPath, true);
|
||||
|
||||
for (var i = 0; i < entityTypes.Length; i++)
|
||||
{
|
||||
|
|
@ -373,7 +378,10 @@ namespace Server
|
|||
EntityPersistence.WriteEntities(itemIndexInfo, Items, ItemTypes, basePath, out var itemCounts);
|
||||
EntityPersistence.WriteEntities(guildIndexInfo, Guilds, GuildTypes, basePath, out var guildCounts);
|
||||
|
||||
TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList());
|
||||
if (_enableSaveStats)
|
||||
{
|
||||
TraceSave(mobileCounts?.ToList(), itemCounts?.ToList(), guildCounts?.ToList());
|
||||
}
|
||||
}
|
||||
|
||||
public static void WriteFiles(object state)
|
||||
|
|
@ -652,13 +660,24 @@ namespace Server
|
|||
Serial serial = reader.ReadSerial();
|
||||
var typeT = typeof(T);
|
||||
|
||||
T entity;
|
||||
|
||||
// Add to this list when creating new serializable types
|
||||
if (typeof(BaseGuild).IsAssignableTo(typeT))
|
||||
{
|
||||
return FindGuild(serial) as T;
|
||||
entity = FindGuild(serial) as T;
|
||||
}
|
||||
else
|
||||
{
|
||||
entity = FindEntity<IEntity>(serial) as T;
|
||||
}
|
||||
|
||||
return FindEntity<IEntity>(serial) as T;
|
||||
if (entity?.Deleted == false)
|
||||
{
|
||||
return entity;
|
||||
}
|
||||
|
||||
return entity?.Created <= reader.LastSerialized ? entity : null;
|
||||
}
|
||||
|
||||
public static List<T> ReadEntityList<T>(this IGenericReader reader) where T : class, ISerializable
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue