* Fixes pooled timer leaking
* Fixes `[dumptimers` command so it outputs properly, adds spacing, and stacktraces
* Adds `[Tidy]` for serializing Lists. This will remove deleted entities during world save before serializing the list.
* Adds helpers for managing Lists/Sets/Dictionaries
### New API
```cs
// Creates the list if it is null, then adds
Utility.Add(ref list, value);
Utility.Add(ref set, value);
Utility.Add(ref dict, key, value);
// Nulls the variable if the count is zero
Utility.Remove(ref list, value);
Utility.Remove(ref set, value);
Utility.Remove(ref dict, key);
// Marks entity as dirty in addition to doing the action
entity.Add(list, value);
// Marks entity as dirty, and will create list if it doesn't exist
entity.Add(ref list, value);
// Marks entity as dirty in addition to doing the action
entity.Remove(list, value);
// Marks entity as dirty, and will null the list count is zero
entity.Remove(ref list, value);
```
### Updates to [dumptimers
<img width="825" alt="Screen Shot 2021-08-14 at 2 55 10 AM" src="https://user-images.githubusercontent.com/3953314/129442449-ccf7fe14-29d6-4f3f-9366-c8eb7b9828a7.png">
- Adds `[ManualDirtyChecking]` for anyone that adds it themselves.
- Adds detection of `[Serializable]` and `[ManualDirtyChecking]` at startup to help identify scripts that need migration.
### Additions
- Automatically opts-out `Item/Mobile/Guild/Accounts` from dirty checking with a new property `UseDirtyChecking`
- Codegen now enables `UseDirtyChecking` via getter. This requires that the property is `virtual` for derived types.
### Fixes
- Fixes `EncodedInt` being broken
- Fixes issues with new custom serializable types that are not derived from Item/Mobile/etc.
- Removes double dirty checking.
### Example of a brand new serializable type that isn't an Item/Mobile/etc.
User created code:
```cs
using System;
namespace Server.Items
{
[Serializable(0)]
public partial class NewTestEntityObject : ISerializable
{
[EncodedInt]
[SerializableField(0)]
private int _someProperty;
public NewTestEntityObject()
{
SetTypeRef(GetType());
// Add to serial tracking like World.Item
/*
Serial = World.NewEntity;
World.AddEntity(this);
*/
}
[AfterDeserialization]
private void AfterDeserialization()
{
Console.WriteLine("This ran!");
}
public int TypeRef { get; }
public Serial Serial { get; }
public void Delete()
{
}
public bool Deleted { get; set; }
public void SetTypeRef(Type type)
{
// Type tracking for persistence goes here
/*
TypeRef = World.NewEntityTypes.IndexOf(type);
if (TypeRef == -1)
{
World.NewEntityTypes.Add(type);
TypeRef = World.NewEntityTypes.Count - 1;
}
*/
}
}
}
```
Generated code:
```cs
namespace Server.Items
{
public partial class NewTestEntityObject
{
#pragma warning disable 0414
private const int _version = 0;
#pragma warning restore 0414
public int SomeProperty
{
get => _someProperty;
set
{
if (value != _someProperty)
{
_someProperty = value;
((ISerializable)this).MarkDirty();
}
}
}
long ISerializable.SavePosition { get; set; } = -1;
BufferWriter ISerializable.SaveBuffer { get; set; }
bool ISerializable.UseDirtyChecking => true;
public NewTestEntityObject(Serial serial)
{
Serial = serial;
SetTypeRef(typeof(NewTestEntityObject));
}
public void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(_version);
writer.WriteEncodedInt(SomeProperty);
}
public void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
SomeProperty = reader.ReadEncodedInt();
Timer.DelayCall(AfterDeserialization);
}
}
}
```
### Features
* Fully abstracts serialization by using compile-time attributes.
* Supports serializing the following:
- Primitives (integers, strings, etc)
- IP Addresses
- BigDecimal
- DateTime, Delta DateTimes
- TimeSpan
- Server.Race
- Server.Map
- Point2D, Point3D, Rect2D, Rect3D
- Existing/New `ISerializable` references
- Lists/Sets of serializable types
- Type with a `Serialize` method and constructor that takes an `IGenericReader`
* Supports forward-only migration
* Supports existing RunUO deserialization for older versions by changing to the following signature:
- `public void OldDeserialize(IGenericReader reader, int version)`
- Must remove deserializing the version since this is already done
* Supports serializing from private fields or custom made properties.
* Types do not require inheriting Item/Mobile. Code gen will fully create `ISerializable` information.
- This is not recommended yet, since it requires wiring to `Persistence` which will cause lots of unresolved symbol errors until code gen is built.
### Example
```cs
using System.Collections.Generic;
namespace Server.Items
{
[Serializable(1)]
public partial class TestItem1 : Item
{
[SerializableField(1)]
[SerializableFieldAttr("[CommandProperty(AccessLevel.Administrator)]")]
private List<Item> _someProperty;
private void Deserialize(IGenericReader reader, int version)
{
}
}
}
```
Generates this:
```cs
namespace Server.Items
{
public partial class TestItem1
{
#pragma warning disable 0414
private const int _version = 1;
#pragma warning restore 0414
[CommandProperty(AccessLevel.Administrator)]
public System.Collections.Generic.List<Server.Item> SomeProperty
{
get => _someProperty;
set
{
if (value != _someProperty)
{
((ISerializable)this).MarkDirty();
_someProperty = value;
}
}
}
public TestItem1(Serial serial) : base(serial)
{
}
public override void Serialize(IGenericWriter writer)
{
var savePosition = ((Server.ISerializable)this).SavePosition;
if (savePosition > -1)
{
writer.Seek(savePosition, System.IO.SeekOrigin.Begin);
return;
}
writer.WriteEncodedInt(_version);
writer.Write(_someProperty);
}
public override void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
if (version < 1)
{
OldDeserialize(reader, version);
((Server.ISerializable)this).MarkDirty();
return;
}
SomeProperty = reader.ReadEntityList<Server.Item>();
}
}
}
```
And this:
```json
{
"version": 1,
"type": "TestItem1",
"properties": [
{
"name": "SomeProperty",
"type": "System.Collections.Generic.List\u003CServer.Item\u003E",
"rule": "ListMigrationRule",
"ruleArguments": [
"Server.Item",
"SerializableInterfaceMigrationRule"
]
}
]
}
```
- [X] Fixes an issue where a buffer smaller than 8 bytes would not double with enough space in some cases.
- [X] Fixes an issue with dupe copying the savebuffer reference (ugh).
- [X] Streamlines the IGenericWriter API to use better generics.
- [X] Streamlines the IGenericReader API to use better generics.
- [X] Forces `tidying` of a List/HashSet to be done externally since Writers/Readers should not have side effects.
- [X] Fixes an issue where Tidying a list didn't TrimExcess, causing memory leaks.
- [X] Reverted the meaning of `World.Running` to specifically refer to any world state post world loading.
- NOTE: Do not use this if you want to block on world saves. Instead use checks against `WorldState.Saving` states.
- [X] Fixes an issue with serializing negative DateTime deltas.
- [X] Fixes a potential issue with serializing non-UTC DateTime.
Bumps release version
- [X] Fixes bugs in the buffer writer
- [X] Removes background save commands
- [X] Avoids calling Serialize() on deserialization
- In a future optimization I will copy the entire object to a buffer, deserialize using a SpanReader, then use that buffer for the SaveBuffer
- [X] Fixes issue with decay queue.
Bumps release version
- [X] Removes all save strategies
- [X] Removes duplicate file writer that won't be used
- [X] Removes persistence (it will become a duplicate system)
- [X] Add a save position variable to skip serializing a clean item/mobile
- [X] Update Guilds/Accounts to be IEntity types
- Because guilds are abstract, this may make serialization tricky.
- [X] Update the generalized IEntity writing
- [X] Update the load/save to write to buffers then to files in background
Bumps release version