diff --git a/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj b/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj
index 8fe1b6a..552ed73 100644
--- a/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj
+++ b/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj
@@ -4,8 +4,8 @@
ModernUO.Serialization.Annotations
netstandard2.0
preview
- 4.1.1
- 4.1.1
+ 4.2.0
+ 4.2.0
ModernUO.Serialization.Annotations
ModernUO.Serialization
true
diff --git a/ModernUO.Serialization.Generator.Tests/DataStructureMethodTests.cs b/ModernUO.Serialization.Generator.Tests/DataStructureMethodTests.cs
new file mode 100644
index 0000000..faa2c75
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/DataStructureMethodTests.cs
@@ -0,0 +1,98 @@
+using System.Runtime.CompilerServices;
+using ModernUO.Serialization.Generator.Tests.Helpers;
+using Xunit;
+
+namespace ModernUO.Serialization.Generator.Tests;
+
+///
+/// Runs the generated AddToX/RemoveFromX/InsertIntoX/ReplaceInX/ClearX helpers against the
+/// Snapshots/NullableCollections entity: removals and clears tolerate a null collection, adds create
+/// it lazily (keeping a [SortedSetComparer]), and MarkDirty fires only when the collection changed.
+///
+public class DataStructureMethodTests
+{
+ private static string FixtureSource([CallerFilePath] string thisFile = "") =>
+ File.ReadAllText(
+ Path.Combine(Path.GetDirectoryName(thisFile)!, "Snapshots", "NullableCollections", "Input.cs")
+ );
+
+ private const string Driver = """
+ namespace Server.TestContent
+ {
+ public static class Driver
+ {
+ public static string Run()
+ {
+ var item = new NullableCollectionsItem();
+ var log = new System.Text.StringBuilder();
+ void Step(string name, object? state = null) =>
+ log.Append(name).Append('=').Append(item.DirtyCount).Append(state != null ? $":{state}" : "").Append(';');
+
+ item.RemoveFromCharges(1);
+ item.RemoveFromChargesAt(0);
+ item.ClearCharges();
+ item.RemoveFromKeywords("a");
+ item.ClearKeywords();
+ item.RemoveFromLabels(1);
+ item.ClearLabels();
+ item.RemoveFromNames("a");
+ item.ClearNames();
+ Step("null", item.Charges == null && item.Keywords == null && item.Labels == null && item.Names == null);
+
+ item.AddToCharges(5);
+ Step("addList");
+ item.InsertIntoCharges(0, 4);
+ Step("insert", string.Join(",", item.Charges!));
+
+ item.AddToKeywords("a");
+ Step("addSet");
+ item.AddToKeywords("a");
+ Step("addSetDuplicate");
+
+ item.AddToLabels(1, "x");
+ Step("addDictionary");
+ item.AddToLabels(1, "y");
+ Step("addDictionaryDuplicate", item.Labels![1]);
+ item.ReplaceInLabels(1, null);
+ Step("replace", item.Labels[1] ?? "null");
+
+ item.AddToNames("Bob");
+ item.AddToNames("BOB");
+ Step("comparer", item.Names!.Count);
+
+ item.RemoveFromKeywords("missing");
+ Step("removeMissing");
+ item.RemoveFromKeywords("a");
+ Step("remove");
+ item.ClearKeywords();
+ Step("clearEmpty");
+ item.ClearCharges();
+ Step("clear", item.Charges.Count);
+
+ var fresh = new NullableCollectionsItem();
+ fresh.ReplaceInLabels(2, "z");
+ log.Append("replaceNull=").Append(fresh.DirtyCount).Append(':').Append(fresh.Labels![2]);
+
+ return log.ToString();
+ }
+ }
+ }
+ """;
+
+ [Fact]
+ public void Helpers_TolerateNullAndMarkDirtyOnlyOnChange()
+ {
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad("NullableCollections", FixtureSource() + Driver);
+
+ var result = (string)assembly.GetType("Server.TestContent.Driver")!
+ .GetMethod("Run")!
+ .Invoke(null, null)!;
+
+ Assert.Equal(
+ "null=0:True;addList=1;insert=2:4,5;addSet=3;addSetDuplicate=3;addDictionary=4;" +
+ "addDictionaryDuplicate=4:x;replace=5:null;comparer=6:1;removeMissing=6;remove=7;" +
+ "clearEmpty=7;clear=8:0;replaceNull=1:z",
+ result
+ );
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Helpers/SourceGeneratorTestHelper.cs b/ModernUO.Serialization.Generator.Tests/Helpers/SourceGeneratorTestHelper.cs
index 036f9eb..a04abeb 100644
--- a/ModernUO.Serialization.Generator.Tests/Helpers/SourceGeneratorTestHelper.cs
+++ b/ModernUO.Serialization.Generator.Tests/Helpers/SourceGeneratorTestHelper.cs
@@ -300,10 +300,26 @@ public static Assembly CompileAndLoad(
return Assembly.Load(stream.ToArray());
}
+ ///
+ /// Runs the generator in schema mode, as ModernUOSchemaGenerator does, and returns each migration
+ /// file it would write as (fileName, json), ready to feed back in as additional texts.
+ ///
+ public static List<(string fileName, string content)> GenerateMigrationSchemas(string assemblyName, string sourceCode)
+ {
+ var generator = new EntitySerializationGenerator(true);
+ RunGeneratorOnCompilation(assemblyName, sourceCode, null, generator);
+
+ var options = SerializableMigrationSchema.GetJsonSerializerOptions();
+ return generator.Migrations.Values
+ .Select(m => ($"{m.Type}.v{m.Version}.json", System.Text.Json.JsonSerializer.Serialize(m, options)))
+ .ToList();
+ }
+
private static (ImmutableArray Diagnostics, Compilation OutputCompilation) RunGeneratorOnCompilation(
string assemblyName,
string sourceCode,
- IEnumerable<(string fileName, string content)>? additionalTexts)
+ IEnumerable<(string fileName, string content)>? additionalTexts,
+ EntitySerializationGenerator? generator = null)
{
var syntaxTrees = new List
{
@@ -328,7 +344,7 @@ private static (ImmutableArray Diagnostics, Compilation OutputCompil
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary)
);
- var generator = new EntitySerializationGenerator();
+ generator ??= new EntitySerializationGenerator();
var additionalTextsList = new List();
if (additionalTexts != null)
diff --git a/ModernUO.Serialization.Generator.Tests/MigrationSaveFlagTests.cs b/ModernUO.Serialization.Generator.Tests/MigrationSaveFlagTests.cs
index 62a4372..2f7d119 100644
--- a/ModernUO.Serialization.Generator.Tests/MigrationSaveFlagTests.cs
+++ b/ModernUO.Serialization.Generator.Tests/MigrationSaveFlagTests.cs
@@ -13,7 +13,7 @@ public class MigrationSaveFlagTests
{
// Binary reader/writer that sizes enums by their underlying type, like ModernUO's, so a width
// mismatch between writer and content struct misaligns the stream instead of going unnoticed.
- private const string BinaryStreams = """
+ internal const string BinaryStreams = """
namespace Server.TestContent
{
using System;
@@ -44,10 +44,11 @@ public void Write(string value)
public void Write(float value) => _writer.Write(value);
public void Write(double value) => _writer.Write(value);
public void Write(decimal value) => _writer.Write(value);
- public void Write(DateTime value) => throw new NotSupportedException();
- public void WriteDeltaTime(DateTime value) => throw new NotSupportedException();
- public void WriteAnchoredTime(DateTime value) => throw new NotSupportedException();
- public void Write(TimeSpan value) => throw new NotSupportedException();
+ // Every time format is raw ticks here; only the stream alignment matters.
+ public void Write(DateTime value) => _writer.Write(value.Ticks);
+ public void WriteDeltaTime(DateTime value) => _writer.Write(value.Ticks);
+ public void WriteAnchoredTime(DateTime value) => _writer.Write(value.Ticks);
+ public void Write(TimeSpan value) => _writer.Write(value.Ticks);
public void Write(Guid value) => throw new NotSupportedException();
public void WriteEncodedInt(int value) => _writer.Write7BitEncodedInt(value);
public void Write(T value) where T : struct, Enum => WriteEnum(value);
@@ -89,10 +90,10 @@ public sealed class BinaryGenericReader : Server.IGenericReader
public float ReadFloat() => _reader.ReadSingle();
public double ReadDouble() => _reader.ReadDouble();
public decimal ReadDecimal() => _reader.ReadDecimal();
- public DateTime ReadDateTime() => throw new NotSupportedException();
- public DateTime ReadDeltaTime() => throw new NotSupportedException();
- public DateTime ReadAnchoredTime() => throw new NotSupportedException();
- public TimeSpan ReadTimeSpan() => throw new NotSupportedException();
+ public DateTime ReadDateTime() => new(_reader.ReadInt64());
+ public DateTime ReadDeltaTime() => new(_reader.ReadInt64());
+ public DateTime ReadAnchoredTime() => new(_reader.ReadInt64());
+ public TimeSpan ReadTimeSpan() => new(_reader.ReadInt64());
public Guid ReadGuid() => throw new NotSupportedException();
public int ReadEncodedInt() => _reader.Read7BitEncodedInt();
@@ -143,6 +144,131 @@ public void MigrateFrom_ReadsEveryFieldWrittenByTheLiveGenerator(int flagCount)
Assert.Equal($"tag|{expected}", migrated);
}
+ // An absent timer flag must leave the content at the "no timer was running" sentinels
+ // (Next == DateTime.MinValue, Delay == TimeSpan.MinValue); a present one resumes its delay.
+ // Covers an anchored timer and a wall-clock one; the trailing flagged int proves alignment.
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void MigrateFrom_SeesTimerSentinelsWhenTheSaveFlagIsAbsent(bool running)
+ {
+ var bytes = WriteTimerV0(running);
+ var migrated = ReadTimerAsV1(running, bytes);
+
+ var timer = running ? "True,False,False" : "False,True,True";
+ Assert.Equal($"{timer}|{timer}|7", migrated);
+ }
+
+ private const string TimerEntityMembers = """
+ public FlaggedTimerItem() { }
+ public System.DateTime Created { get; set; }
+ public Server.Serial Serial { get; }
+ public bool Deleted => false;
+ public void Delete() { }
+ """;
+
+ private static byte[] WriteTimerV0(bool running)
+ {
+ var timerValue = running ? "new Server.Timer { Next = Server.Core.Now.AddHours(1) }" : "null";
+ var source = $$"""
+ using ModernUO.Serialization;
+ namespace Server.TestContent {
+ [SerializationGenerator(0)]
+ public partial class FlaggedTimerItem : Server.ISerializable {
+ [SerializableField(0)]
+ [SaveFlag(nameof(ShouldSerializeAnchored))]
+ [DeserializeTimer(nameof(RestartAnchored))]
+ private Server.Timer _anchoredTimer;
+ private bool ShouldSerializeAnchored() => _anchoredTimer != null;
+ private void RestartAnchored(System.TimeSpan delay) { }
+
+ [SerializableField(1)]
+ [SaveFlag(nameof(ShouldSerializeDeadline))]
+ [DeserializeTimer(nameof(RestartDeadline), wallClock: true)]
+ private Server.Timer _deadlineTimer;
+ private bool ShouldSerializeDeadline() => _deadlineTimer != null;
+ private void RestartDeadline(System.TimeSpan delay) { }
+
+ [SerializableField(2)]
+ [SaveFlag(nameof(ShouldSerializeCount))]
+ private int _count;
+ private bool ShouldSerializeCount() => _count != 0;
+
+ {{TimerEntityMembers}}
+
+ public static byte[] WriteSample() {
+ var item = new FlaggedTimerItem { _anchoredTimer = {{timerValue}}, _deadlineTimer = {{timerValue}}, _count = 7 };
+ var stream = new System.IO.MemoryStream();
+ var writer = new BinaryGenericWriter(stream);
+ item.Serialize(writer);
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+ }
+ """;
+
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad($"FlaggedTimerV0_{running}", source + BinaryStreams);
+
+ return (byte[])assembly.GetType("Server.TestContent.FlaggedTimerItem")!
+ .GetMethod("WriteSample")!
+ .Invoke(null, null)!;
+ }
+
+ private static string ReadTimerAsV1(bool running, byte[] bytes)
+ {
+ var source = $$"""
+ using System;
+ using ModernUO.Serialization;
+ namespace Server.TestContent {
+ [SerializationGenerator(1)]
+ public partial class FlaggedTimerItem : Server.ISerializable {
+ [SerializableField(0)] private string _migrated;
+
+ {{TimerEntityMembers}}
+
+ private static string Describe(DateTime next, TimeSpan delay) =>
+ $"{delay > TimeSpan.Zero},{next == DateTime.MinValue},{delay == TimeSpan.MinValue}";
+
+ private void MigrateFrom(V0Content content) {
+ _migrated = Describe(content.AnchoredTimerNext, content.AnchoredTimerDelay) + "|" +
+ Describe(content.DeadlineTimerNext, content.DeadlineTimerDelay) + "|" + content.Count;
+ }
+
+ public static string ReadSample(byte[] bytes) {
+ var reader = new BinaryGenericReader(new System.IO.MemoryStream(bytes));
+ var item = new FlaggedTimerItem();
+ item.Deserialize(reader);
+ if (!reader.AtEnd) throw new InvalidOperationException("Unread bytes remain");
+ return item._migrated;
+ }
+ }
+ }
+ """;
+
+ const string json = """
+ {
+ "version": 0,
+ "type": "Server.TestContent.FlaggedTimerItem",
+ "properties": [
+ { "name": "AnchoredTimer", "type": "Server.Timer", "usesSaveFlag": true, "rule": "TimerMigrationRule", "ruleArguments": ["@AnchoredTimer"] },
+ { "name": "DeadlineTimer", "type": "Server.Timer", "usesSaveFlag": true, "rule": "TimerMigrationRule", "ruleArguments": [""] },
+ { "name": "Count", "type": "int", "usesSaveFlag": true, "rule": "PrimitiveTypeMigrationRule" }
+ ]
+ }
+ """;
+
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad(
+ $"FlaggedTimerV1_{running}",
+ source + BinaryStreams,
+ [("Server.TestContent.FlaggedTimerItem.v0.json", json)]
+ );
+
+ return (string)assembly.GetType("Server.TestContent.FlaggedTimerItem")!
+ .GetMethod("ReadSample")!
+ .Invoke(null, [bytes])!;
+ }
+
private static byte[] WriteV0(int flagCount)
{
var source = new StringBuilder();
diff --git a/ModernUO.Serialization.Generator.Tests/NullableValueTypeTests.cs b/ModernUO.Serialization.Generator.Tests/NullableValueTypeTests.cs
new file mode 100644
index 0000000..947d8f8
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/NullableValueTypeTests.cs
@@ -0,0 +1,193 @@
+using ModernUO.Serialization.Generator.Tests.Helpers;
+using Xunit;
+
+namespace ModernUO.Serialization.Generator.Tests;
+
+///
+/// Nullable<T> fields (NullableMigrationRule) round trip through the live generator, both as
+/// fields and nested in collections, and through a migration content struct. A trailing string and an
+/// unread-bytes check prove the HasValue prefixes keep the stream aligned.
+///
+public class NullableValueTypeTests
+{
+ private const string EntityMembers = """
+ public System.DateTime Created { get; set; }
+ public Server.Serial Serial { get; }
+ public bool Deleted => false;
+ public void Delete() { }
+ """;
+
+ private const string LiveEntity = $$"""
+ using System;
+ using System.Collections.Generic;
+ using ModernUO.Serialization;
+ namespace Server.TestContent {
+ public enum Mood { Calm, Angry }
+
+ [SerializationGenerator(0)]
+ public partial struct Stats
+ {
+ [SerializableField(0)] private int _strength;
+ }
+
+ [SerializationGenerator(0)]
+ public partial class NullableValuesItem : Server.ISerializable {
+ [SerializableField(0)] private int? _count;
+ [SerializableField(1)] [EncodedInt] private int? _encoded;
+ [SerializableField(2)] private Mood? _mood;
+ [SerializableField(3)] private TimeSpan? _cooldown;
+ [SerializableField(4)] private Stats? _stats;
+ [SerializableField(5)] private List _samples;
+ [SerializableField(6)] private Dictionary _weights;
+ [SerializableField(7)] [SaveFlag(nameof(ShouldSerializeBonus))] private int? _bonus;
+ private bool ShouldSerializeBonus() => _bonus != null;
+ [SerializableField(8)] private string _tail;
+
+ public NullableValuesItem() { }
+ {{EntityMembers}}
+
+ private static string Show(T? value) where T : struct => value?.ToString() ?? "null";
+
+ private string Describe() =>
+ $"{Show(_count)},{Show(_encoded)},{Show(_mood)},{Show(_cooldown)},{Show(_stats?.Strength)}," +
+ $"[{string.Join(",", _samples.ConvertAll(Show))}],{Show(_weights[1])},{Show(_bonus)},{_tail}";
+
+ public static string RoundTrip(bool populated) {
+ var item = new NullableValuesItem { _samples = [], _weights = new() { [1] = null }, _tail = "end" };
+ if (populated) {
+ item._count = -7;
+ item._encoded = 300;
+ item._mood = Server.TestContent.Mood.Angry;
+ item._cooldown = TimeSpan.FromSeconds(90);
+ item._stats = new Stats { Strength = 12 };
+ item._samples = [1, null, 3];
+ item._weights[1] = 2.5;
+ item._bonus = 4;
+ }
+
+ var stream = new System.IO.MemoryStream();
+ var writer = new BinaryGenericWriter(stream);
+ item.Serialize(writer);
+ writer.Flush();
+
+ var reader = new BinaryGenericReader(new System.IO.MemoryStream(stream.ToArray()));
+ var copy = new NullableValuesItem();
+ copy.Deserialize(reader);
+ if (!reader.AtEnd) throw new InvalidOperationException("Unread bytes remain");
+ return copy.Describe();
+ }
+ }
+ }
+ """;
+
+ [Theory]
+ [InlineData(true, "-7,300,Angry,00:01:30,12,[1,null,3],2.5,4,end")]
+ [InlineData(false, "null,null,null,null,null,[],null,null,end")]
+ public void LiveRoundTrip_PreservesValuesAndNulls(bool populated, string expected)
+ {
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad(
+ $"NullableValuesLive_{populated}",
+ LiveEntity + MigrationSaveFlagTests.BinaryStreams
+ );
+
+ var result = (string)assembly.GetType("Server.TestContent.NullableValuesItem")!
+ .GetMethod("RoundTrip")!
+ .Invoke(null, [populated])!;
+
+ Assert.Equal(expected, result);
+ }
+
+ // The v1 side reads the schema that schema mode generates for the v0 entity, as the tool would.
+ [Theory]
+ [InlineData(true, "5|6|end")]
+ [InlineData(false, "null|null|end")]
+ public void MigrateFrom_ReadsNullableFieldsWrittenByTheLiveGenerator(bool populated, string expected)
+ {
+ var v0Source = V0Source(populated);
+ var schemas = SourceGeneratorTestHelper.GenerateMigrationSchemas($"MigratingNullableSchema_{populated}", v0Source);
+
+ Assert.Contains(
+ "\"rule\": \"NullableMigrationRule\"",
+ Assert.Single(schemas, s => s.fileName == "Server.TestContent.MigratingNullableItem.v0.json").content
+ );
+
+ var bytes = WriteV0(populated, v0Source);
+ Assert.Equal(expected, ReadAsV1(populated, bytes, schemas));
+ }
+
+ private static string V0Source(bool populated)
+ {
+ var values = populated ? "_count = 5, _bonus = 6," : "";
+ return $$"""
+ using ModernUO.Serialization;
+ namespace Server.TestContent {
+ [SerializationGenerator(0)]
+ public partial class MigratingNullableItem : Server.ISerializable {
+ [SerializableField(0)] private int? _count;
+ [SerializableField(1)] [SaveFlag(nameof(ShouldSerializeBonus))] private int? _bonus;
+ private bool ShouldSerializeBonus() => _bonus != null;
+ [SerializableField(2)] private string _tail;
+
+ public MigratingNullableItem() { }
+ {{EntityMembers}}
+
+ public static byte[] WriteSample() {
+ var item = new MigratingNullableItem { {{values}} _tail = "end" };
+ var stream = new System.IO.MemoryStream();
+ var writer = new BinaryGenericWriter(stream);
+ item.Serialize(writer);
+ writer.Flush();
+ return stream.ToArray();
+ }
+ }
+ }
+ """ + MigrationSaveFlagTests.BinaryStreams;
+ }
+
+ private static byte[] WriteV0(bool populated, string v0Source)
+ {
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad($"MigratingNullableV0_{populated}", v0Source);
+
+ return (byte[])assembly.GetType("Server.TestContent.MigratingNullableItem")!
+ .GetMethod("WriteSample")!
+ .Invoke(null, null)!;
+ }
+
+ private static string ReadAsV1(bool populated, byte[] bytes, List<(string fileName, string content)> schemas)
+ {
+ var source = $$"""
+ using ModernUO.Serialization;
+ namespace Server.TestContent {
+ [SerializationGenerator(1)]
+ public partial class MigratingNullableItem : Server.ISerializable {
+ [SerializableField(0)] private string _migrated;
+
+ public MigratingNullableItem() { }
+ {{EntityMembers}}
+
+ private void MigrateFrom(V0Content content) {
+ _migrated = $"{content.Count?.ToString() ?? "null"}|{content.Bonus?.ToString() ?? "null"}|{content.Tail}";
+ }
+
+ public static string ReadSample(byte[] bytes) {
+ var reader = new BinaryGenericReader(new System.IO.MemoryStream(bytes));
+ var item = new MigratingNullableItem();
+ item.Deserialize(reader);
+ if (!reader.AtEnd) throw new System.InvalidOperationException("Unread bytes remain");
+ return item._migrated;
+ }
+ }
+ }
+ """;
+
+ var assembly = SourceGeneratorTestHelper.CompileAndLoad(
+ $"MigratingNullableV1_{populated}",
+ source + MigrationSaveFlagTests.BinaryStreams,
+ schemas
+ );
+
+ return (string)assembly.GetType("Server.TestContent.MigratingNullableItem")!
+ .GetMethod("ReadSample")!
+ .Invoke(null, [bytes])!;
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Collections/Expected/Server.TestContent.CollectionsItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Collections/Expected/Server.TestContent.CollectionsItem.Serialization.g.cs
index e41debb..d239fea 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/Collections/Expected/Server.TestContent.CollectionsItem.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Collections/Expected/Server.TestContent.CollectionsItem.Serialization.g.cs
@@ -50,32 +50,42 @@ public System.Collections.Generic.List Charges
public void AddToCharges(int value)
{
- Charges.Add(value);
+ _charges ??= new System.Collections.Generic.List();
+ _charges.Add(value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromCharges(int value)
{
- Charges.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_charges?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void InsertIntoCharges(int index, int value)
{
- Charges.Insert(index, value);
+ _charges ??= new System.Collections.Generic.List();
+ _charges.Insert(index, value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromChargesAt(int index)
{
- Charges.RemoveAt(index);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_charges != null)
+ {
+ _charges.RemoveAt(index);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ClearCharges()
{
- Charges.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_charges?.Count > 0)
+ {
+ _charges.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public System.Collections.Generic.HashSet Keywords
@@ -93,21 +103,28 @@ public System.Collections.Generic.HashSet Keywords
public void AddToKeywords(string value)
{
- Keywords.Add(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ _keywords ??= new System.Collections.Generic.HashSet();
+ if (_keywords.Add(value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void RemoveFromKeywords(string value)
{
- Keywords.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_keywords?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
-
public void ClearKeywords()
{
- Keywords.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_keywords?.Count > 0)
+ {
+ _keywords.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public System.Collections.Generic.Dictionary Labels
@@ -125,26 +142,35 @@ public System.Collections.Generic.Dictionary Labels
public void AddToLabels(int key, string value)
{
- Labels.Add(key, value);
- Server.ISerializableExtensions.MarkDirty(this);
+ _labels ??= new System.Collections.Generic.Dictionary();
+ if (_labels.TryAdd(key, value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void RemoveFromLabels(int key)
{
- Labels.Remove(key);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_labels?.Remove(key) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ReplaceInLabels(int key, string value)
{
- Labels[key] = value;
+ _labels ??= new System.Collections.Generic.Dictionary();
+ _labels[key] = value;
Server.ISerializableExtensions.MarkDirty(this);
}
public void ClearLabels()
{
- Labels.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_labels?.Count > 0)
+ {
+ _labels.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public CollectionsItem(Server.Serial serial)
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Expected/Server.TestContent.FieldModifiersItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Expected/Server.TestContent.FieldModifiersItem.Serialization.g.cs
index bebd768..d5f7a73 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Expected/Server.TestContent.FieldModifiersItem.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Expected/Server.TestContent.FieldModifiersItem.Serialization.g.cs
@@ -43,26 +43,35 @@ public System.Collections.Generic.Dictionary Entries
public void AddToEntries(int key, string value)
{
- Entries.Add(key, value);
- Server.ISerializableExtensions.MarkDirty(this);
+ _entries ??= new System.Collections.Generic.Dictionary();
+ if (_entries.TryAdd(key, value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void RemoveFromEntries(int key)
{
- Entries.Remove(key);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Remove(key) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ReplaceInEntries(int key, string value)
{
- Entries[key] = value;
+ _entries ??= new System.Collections.Generic.Dictionary();
+ _entries[key] = value;
Server.ISerializableExtensions.MarkDirty(this);
}
public void ClearEntries()
{
- Entries.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Count > 0)
+ {
+ _entries.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public int Level
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Expected/Server.TestContent.FlaggedTimerItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Expected/Server.TestContent.FlaggedTimerItem.Serialization.g.cs
new file mode 100644
index 0000000..b86b4d5
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Expected/Server.TestContent.FlaggedTimerItem.Serialization.g.cs
@@ -0,0 +1,124 @@
+//
+// This code was generated by the ModernUO Serialization Generator tool.
+// Version: {VERSION}
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+
+#pragma warning disable
+
+namespace Server.TestContent
+{
+ [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")]
+ public partial class FlaggedTimerItem
+ {
+ private const int SerializationVersion = 1;
+
+ public int Count
+ {
+ get => _count;
+ set
+ {
+ if (value != _count)
+ {
+ _count = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public FlaggedTimerItem(Server.Serial serial)
+ {
+ Serial = serial;
+ }
+
+ ref struct V0Content
+ {
+ internal readonly int? Count;
+ internal readonly System.DateTime AnchoredTimerNext;
+ internal readonly System.TimeSpan AnchoredTimerDelay;
+ internal readonly System.DateTime DeadlineTimerNext;
+ internal readonly System.TimeSpan DeadlineTimerDelay;
+ internal readonly System.DateTime DriftTimerNext;
+ internal readonly System.TimeSpan DriftTimerDelay;
+
+ [System.Flags]
+ private enum V0SaveFlag
+ {
+ None = 0x00000000,
+ Count = 0x00000001,
+ AnchoredTimer = 0x00000002,
+ DeadlineTimer = 0x00000004,
+ DriftTimer = 0x00000008,
+ }
+ internal V0Content(Server.IGenericReader reader, Server.TestContent.FlaggedTimerItem entity)
+ {
+ var saveFlags = reader.ReadEnum();
+
+ if ((saveFlags & V0SaveFlag.Count) != 0)
+ {
+ Count = reader.ReadInt();
+ }
+ else
+ {
+ Count = default;
+ }
+
+ if ((saveFlags & V0SaveFlag.AnchoredTimer) != 0)
+ {
+ AnchoredTimerNext = reader.ReadAnchoredTime();
+ AnchoredTimerDelay = AnchoredTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : AnchoredTimerNext - Server.Core.Now;
+ }
+ else
+ {
+ AnchoredTimerNext = System.DateTime.MinValue;
+ AnchoredTimerDelay = System.TimeSpan.MinValue;
+ }
+
+ if ((saveFlags & V0SaveFlag.DeadlineTimer) != 0)
+ {
+ DeadlineTimerNext = reader.ReadDateTime();
+ DeadlineTimerDelay = DeadlineTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DeadlineTimerNext - Server.Core.Now;
+ }
+ else
+ {
+ DeadlineTimerNext = System.DateTime.MinValue;
+ DeadlineTimerDelay = System.TimeSpan.MinValue;
+ }
+
+ if ((saveFlags & V0SaveFlag.DriftTimer) != 0)
+ {
+ DriftTimerNext = reader.ReadDeltaTime();
+ DriftTimerDelay = DriftTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DriftTimerNext - Server.Core.Now;
+ }
+ else
+ {
+ DriftTimerNext = System.DateTime.MinValue;
+ DriftTimerDelay = System.TimeSpan.MinValue;
+ }
+ }
+ }
+
+ public virtual void Serialize(Server.IGenericWriter writer)
+ {
+ writer.WriteEncodedInt(SerializationVersion);
+
+ writer.Write(_count);
+ }
+
+ public virtual void Deserialize(Server.IGenericReader reader)
+ {
+ var version = reader.ReadEncodedInt();
+
+ if (version == 0)
+ {
+ MigrateFrom(new V0Content(reader, this));
+ Server.ISerializableExtensions.MarkDirty(this);
+ return;
+ }
+
+ _count = reader.ReadInt();
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Input.cs
new file mode 100644
index 0000000..d8587f1
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Input.cs
@@ -0,0 +1,43 @@
+using System;
+using ModernUO.Serialization;
+using Server;
+
+namespace Server.TestContent
+{
+ // v0 save-flagged its timers, so V0Content must default Next/Delay when a flag is absent.
+ [SerializationGenerator(1)]
+ public partial class FlaggedTimerItem : ISerializable
+ {
+ [SerializableField(0)]
+ private int _count;
+
+ private Timer _anchoredTimer;
+ private Timer _deadlineTimer;
+ private Timer _driftTimer;
+
+ public DateTime Created { get; set; }
+ public Serial Serial { get; }
+ public bool Deleted => false;
+ public void Delete() { }
+
+ private void MigrateFrom(V0Content content)
+ {
+ _count = content.Count ?? 0;
+
+ if (content.AnchoredTimerDelay != TimeSpan.MinValue)
+ {
+ _anchoredTimer = new Timer { Delay = content.AnchoredTimerDelay };
+ }
+
+ if (content.DeadlineTimerDelay != TimeSpan.MinValue)
+ {
+ _deadlineTimer = new Timer { Delay = content.DeadlineTimerDelay };
+ }
+
+ if (content.DriftTimerDelay != TimeSpan.MinValue)
+ {
+ _driftTimer = new Timer { Delay = content.DriftTimerDelay };
+ }
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Server.TestContent.FlaggedTimerItem.v0.json b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Server.TestContent.FlaggedTimerItem.v0.json
new file mode 100644
index 0000000..a1d8f30
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Server.TestContent.FlaggedTimerItem.v0.json
@@ -0,0 +1,39 @@
+{
+ "version": 0,
+ "type": "Server.TestContent.FlaggedTimerItem",
+ "properties": [
+ {
+ "name": "Count",
+ "type": "int",
+ "usesSaveFlag": true,
+ "rule": "PrimitiveTypeMigrationRule"
+ },
+ {
+ "name": "AnchoredTimer",
+ "type": "Server.Timer",
+ "usesSaveFlag": true,
+ "rule": "TimerMigrationRule",
+ "ruleArguments": [
+ "@AnchoredTimer"
+ ]
+ },
+ {
+ "name": "DeadlineTimer",
+ "type": "Server.Timer",
+ "usesSaveFlag": true,
+ "rule": "TimerMigrationRule",
+ "ruleArguments": [
+ ""
+ ]
+ },
+ {
+ "name": "DriftTimer",
+ "type": "Server.Timer",
+ "usesSaveFlag": true,
+ "rule": "TimerMigrationRule",
+ "ruleArguments": [
+ "@TimerDrift"
+ ]
+ }
+ ]
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.ExtendedOwner.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.ExtendedOwner.Serialization.g.cs
index 8f7f149..4be42ac 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.ExtendedOwner.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.ExtendedOwner.Serialization.g.cs
@@ -30,32 +30,42 @@ public System.Collections.Generic.List Extended
public void AddToExtendedEntries(Server.TestContent.DerivedEntry value)
{
- ExtendedEntries.Add(value);
+ _extendedEntries ??= new System.Collections.Generic.List();
+ _extendedEntries.Add(value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromExtendedEntries(Server.TestContent.DerivedEntry value)
{
- ExtendedEntries.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_extendedEntries?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void InsertIntoExtendedEntries(int index, Server.TestContent.DerivedEntry value)
{
- ExtendedEntries.Insert(index, value);
+ _extendedEntries ??= new System.Collections.Generic.List();
+ _extendedEntries.Insert(index, value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromExtendedEntriesAt(int index)
{
- ExtendedEntries.RemoveAt(index);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_extendedEntries != null)
+ {
+ _extendedEntries.RemoveAt(index);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ClearExtendedEntries()
{
- ExtendedEntries.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_extendedEntries?.Count > 0)
+ {
+ _extendedEntries.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public ExtendedOwner(Server.Serial serial) : base(serial)
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.Owner.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.Owner.Serialization.g.cs
index 6528546..c168f12 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.Owner.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NestedInheritance/Expected/Server.TestContent.Owner.Serialization.g.cs
@@ -30,32 +30,42 @@ public System.Collections.Generic.List Entries
public void AddToEntries(Server.TestContent.BaseEntry value)
{
- Entries.Add(value);
+ _entries ??= new System.Collections.Generic.List();
+ _entries.Add(value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromEntries(Server.TestContent.BaseEntry value)
{
- Entries.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void InsertIntoEntries(int index, Server.TestContent.BaseEntry value)
{
- Entries.Insert(index, value);
+ _entries ??= new System.Collections.Generic.List();
+ _entries.Insert(index, value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromEntriesAt(int index)
{
- Entries.RemoveAt(index);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries != null)
+ {
+ _entries.RemoveAt(index);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ClearEntries()
{
- Entries.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Count > 0)
+ {
+ _entries.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public Owner(Server.Serial serial) : base(serial)
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NoDirtyTarget/Expected/Server.TestContent.NoDirtyTargetRecord.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NoDirtyTarget/Expected/Server.TestContent.NoDirtyTargetRecord.Serialization.g.cs
index 515106c..1c9da54 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/NoDirtyTarget/Expected/Server.TestContent.NoDirtyTargetRecord.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NoDirtyTarget/Expected/Server.TestContent.NoDirtyTargetRecord.Serialization.g.cs
@@ -41,27 +41,35 @@ public System.Collections.Generic.List Values
public void AddToValues(int value)
{
- Values.Add(value);
+ _values ??= new System.Collections.Generic.List();
+ _values.Add(value);
}
public void RemoveFromValues(int value)
{
- Values.Remove(value);
+ _values?.Remove(value);
}
public void InsertIntoValues(int index, int value)
{
- Values.Insert(index, value);
+ _values ??= new System.Collections.Generic.List();
+ _values.Insert(index, value);
}
public void RemoveFromValuesAt(int index)
{
- Values.RemoveAt(index);
+ if (_values != null)
+ {
+ _values.RemoveAt(index);
+ }
}
public void ClearValues()
{
- Values.Clear();
+ if (_values?.Count > 0)
+ {
+ _values.Clear();
+ }
}
public System.Collections.Generic.Dictionary Lookup
@@ -78,22 +86,27 @@ public System.Collections.Generic.Dictionary Lookup
public void AddToLookup(int key, string value)
{
- Lookup.Add(key, value);
+ _lookup ??= new System.Collections.Generic.Dictionary();
+ _lookup.TryAdd(key, value);
}
public void RemoveFromLookup(int key)
{
- Lookup.Remove(key);
+ _lookup?.Remove(key);
}
public void ReplaceInLookup(int key, string value)
{
- Lookup[key] = value;
+ _lookup ??= new System.Collections.Generic.Dictionary();
+ _lookup[key] = value;
}
public void ClearLookup()
{
- Lookup.Clear();
+ if (_lookup?.Count > 0)
+ {
+ _lookup.Clear();
+ }
}
public virtual void Serialize(Server.IGenericWriter writer)
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Expected/Server.TestContent.NullableCollectionsItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Expected/Server.TestContent.NullableCollectionsItem.Serialization.g.cs
new file mode 100644
index 0000000..fae98bc
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Expected/Server.TestContent.NullableCollectionsItem.Serialization.g.cs
@@ -0,0 +1,351 @@
+//
+// This code was generated by the ModernUO Serialization Generator tool.
+// Version: {VERSION}
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+
+#pragma warning disable
+
+namespace Server.TestContent
+{
+ [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")]
+ public partial class NullableCollectionsItem
+ {
+ private const int SerializationVersion = 0;
+
+ public string? Title
+ {
+ get => _title;
+ set
+ {
+ if (value != _title)
+ {
+ _title = value;
+ MarkDirty();
+ }
+ }
+ }
+
+ public System.Collections.Generic.List? Charges
+ {
+ get => _charges;
+ set
+ {
+ if (value != _charges)
+ {
+ _charges = value;
+ MarkDirty();
+ }
+ }
+ }
+
+ public void AddToCharges(int value)
+ {
+ _charges ??= new System.Collections.Generic.List();
+ _charges.Add(value);
+ MarkDirty();
+ }
+
+ public void RemoveFromCharges(int value)
+ {
+ if (_charges?.Remove(value) == true)
+ {
+ MarkDirty();
+ }
+ }
+
+ public void InsertIntoCharges(int index, int value)
+ {
+ _charges ??= new System.Collections.Generic.List();
+ _charges.Insert(index, value);
+ MarkDirty();
+ }
+
+ public void RemoveFromChargesAt(int index)
+ {
+ if (_charges != null)
+ {
+ _charges.RemoveAt(index);
+ MarkDirty();
+ }
+ }
+
+ public void ClearCharges()
+ {
+ if (_charges?.Count > 0)
+ {
+ _charges.Clear();
+ MarkDirty();
+ }
+ }
+
+ public System.Collections.Generic.HashSet? Keywords
+ {
+ get => _keywords;
+ set
+ {
+ if (value != _keywords)
+ {
+ _keywords = value;
+ MarkDirty();
+ }
+ }
+ }
+
+ public void AddToKeywords(string value)
+ {
+ _keywords ??= new System.Collections.Generic.HashSet();
+ if (_keywords.Add(value))
+ {
+ MarkDirty();
+ }
+ }
+
+ public void RemoveFromKeywords(string value)
+ {
+ if (_keywords?.Remove(value) == true)
+ {
+ MarkDirty();
+ }
+ }
+
+ public void ClearKeywords()
+ {
+ if (_keywords?.Count > 0)
+ {
+ _keywords.Clear();
+ MarkDirty();
+ }
+ }
+
+ public System.Collections.Generic.Dictionary? Labels
+ {
+ get => _labels;
+ set
+ {
+ if (value != _labels)
+ {
+ _labels = value;
+ MarkDirty();
+ }
+ }
+ }
+
+ public void AddToLabels(int key, string? value)
+ {
+ _labels ??= new System.Collections.Generic.Dictionary();
+ if (_labels.TryAdd(key, value))
+ {
+ MarkDirty();
+ }
+ }
+
+ public void RemoveFromLabels(int key)
+ {
+ if (_labels?.Remove(key) == true)
+ {
+ MarkDirty();
+ }
+ }
+
+ public void ReplaceInLabels(int key, string? value)
+ {
+ _labels ??= new System.Collections.Generic.Dictionary();
+ _labels[key] = value;
+ MarkDirty();
+ }
+
+ public void ClearLabels()
+ {
+ if (_labels?.Count > 0)
+ {
+ _labels.Clear();
+ MarkDirty();
+ }
+ }
+
+ public System.Collections.Generic.SortedSet? Names
+ {
+ get => _names;
+ set
+ {
+ if (value != _names)
+ {
+ _names = value;
+ MarkDirty();
+ }
+ }
+ }
+
+ public void AddToNames(string value)
+ {
+ _names ??= new System.Collections.Generic.SortedSet(new Server.TestContent.CaseInsensitiveComparer());
+ if (_names.Add(value))
+ {
+ MarkDirty();
+ }
+ }
+
+ public void RemoveFromNames(string value)
+ {
+ if (_names?.Remove(value) == true)
+ {
+ MarkDirty();
+ }
+ }
+
+ public void ClearNames()
+ {
+ if (_names?.Count > 0)
+ {
+ _names.Clear();
+ MarkDirty();
+ }
+ }
+
+ public string?[]? Aliases
+ {
+ get => _aliases;
+ set
+ {
+ if (value != _aliases)
+ {
+ _aliases = value;
+ MarkDirty();
+ }
+ }
+ }
+
+
+ public void ClearAliases()
+ {
+ Aliases = System.Array.Empty();
+ MarkDirty();
+ }
+
+ public NullableCollectionsItem(Server.Serial serial)
+ {
+ Serial = serial;
+ }
+
+ public virtual void Serialize(Server.IGenericWriter writer)
+ {
+ writer.WriteEncodedInt(SerializationVersion);
+
+ writer.Write(_title);
+
+ var _chargesCount = _charges?.Count ?? 0;
+ writer.WriteEncodedInt(_chargesCount);
+ if (_chargesCount > 0)
+ {
+ foreach (var _chargesEntry in _charges!)
+ {
+ writer.Write(_chargesEntry);
+ }
+ }
+
+ var _keywordsCount = _keywords?.Count ?? 0;
+ writer.WriteEncodedInt(_keywordsCount);
+ if (_keywordsCount > 0)
+ {
+ foreach (var _keywordsEntry in _keywords!)
+ {
+ writer.Write(_keywordsEntry);
+ }
+ }
+
+ var _labelsCount = _labels?.Count ?? 0;
+ writer.WriteEncodedInt(_labelsCount);
+ if (_labelsCount > 0)
+ {
+ foreach (var (_labelsKey, _labelsValue) in _labels!)
+ {
+ writer.Write(_labelsKey);
+ writer.Write(_labelsValue);
+ }
+ }
+
+ var _namesCount = _names?.Count ?? 0;
+ writer.WriteEncodedInt(_namesCount);
+ if (_namesCount > 0)
+ {
+ foreach (var _namesEntry in _names!)
+ {
+ writer.Write(_namesEntry);
+ }
+ }
+
+ var _aliasesLength = _aliases?.Length ?? 0;
+ writer.WriteEncodedInt(_aliasesLength);
+ for (var _aliasesIndex = 0; _aliasesIndex < _aliasesLength; _aliasesIndex++)
+ {
+ var _aliasesEntry = _aliases![_aliasesIndex];
+ writer.Write(_aliasesEntry);
+ }
+ }
+
+ public virtual void Deserialize(Server.IGenericReader reader)
+ {
+ var version = reader.ReadEncodedInt();
+
+ _title = reader.ReadString();
+
+ int _chargesEntry;
+ var _chargesCount = reader.ReadEncodedInt();
+ _charges = new System.Collections.Generic.List(_chargesCount);
+ for (var _chargesIndex = 0; _chargesIndex < _chargesCount; _chargesIndex++)
+ {
+ _chargesEntry = reader.ReadInt();
+ _charges.Add(_chargesEntry);
+ }
+
+ string _keywordsEntry;
+ var _keywordsCount = reader.ReadEncodedInt();
+ _keywords = new System.Collections.Generic.HashSet(_keywordsCount);
+ for (var _keywordsIndex = 0; _keywordsIndex < _keywordsCount; _keywordsIndex++)
+ {
+ _keywordsEntry = reader.ReadString();
+ if (typeof(string).IsValueType || _keywordsEntry != default)
+ {
+ _keywords.Add(_keywordsEntry);
+ }
+ }
+
+ int _labelsKey;
+ string _labelsValue;
+ var _labelsCount = reader.ReadEncodedInt();
+ _labels = new System.Collections.Generic.Dictionary(_labelsCount);
+ for (var _labelsIndex = 0; _labelsIndex < _labelsCount; _labelsIndex++)
+ {
+ _labelsKey = reader.ReadInt();
+ _labelsValue = reader.ReadString();
+ if (typeof(int).IsValueType || _labelsKey != default)
+ {
+ _labels.Add(_labelsKey, _labelsValue);
+ }
+ }
+
+ string _namesEntry;
+ var _namesCount = reader.ReadEncodedInt();
+ _names = new System.Collections.Generic.SortedSet(new Server.TestContent.CaseInsensitiveComparer());
+ for (var _namesIndex = 0; _namesIndex < _namesCount; _namesIndex++)
+ {
+ _namesEntry = reader.ReadString();
+ if (typeof(string).IsValueType || _namesEntry != default)
+ {
+ _names.Add(_namesEntry);
+ }
+ }
+
+ _aliases = new string[reader.ReadEncodedInt()];
+ for (var _aliasesIndex = 0; _aliasesIndex < _aliases.Length; _aliasesIndex++)
+ {
+ var _aliasesEntry = _aliases![_aliasesIndex];
+ _aliasesEntry = reader.ReadString();
+ _aliases![_aliasesIndex] = _aliasesEntry;
+ }
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Input.cs
new file mode 100644
index 0000000..6b73ce2
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Input.cs
@@ -0,0 +1,48 @@
+#nullable enable
+using System;
+using System.Collections.Generic;
+using ModernUO.Serialization;
+using Server;
+
+namespace Server.TestContent
+{
+ public class CaseInsensitiveComparer : IComparer
+ {
+ public int Compare(string? x, string? y) => string.Compare(x, y, StringComparison.OrdinalIgnoreCase);
+ }
+
+ // Lazily created collections: null until the first AddTo/InsertInto/ReplaceIn.
+ [SerializationGenerator(0)]
+ public partial class NullableCollectionsItem : ISerializable
+ {
+ [SerializableField(0)]
+ private string? _title;
+
+ [SerializableField(1)]
+ private List? _charges;
+
+ [SerializableField(2)]
+ private HashSet? _keywords;
+
+ [SerializableField(3)]
+ private Dictionary? _labels;
+
+ [SerializableField(4)]
+ [SortedSetComparer(typeof(CaseInsensitiveComparer))]
+ private SortedSet? _names;
+
+ [SerializableField(5)]
+ private string?[]? _aliases;
+
+ public int DirtyCount { get; private set; }
+
+ public void MarkDirty() => DirtyCount++;
+
+ public NullableCollectionsItem() { }
+
+ public DateTime Created { get; set; }
+ public Serial Serial { get; }
+ public bool Deleted => false;
+ public void Delete() { }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.NullableValuesItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.NullableValuesItem.Serialization.g.cs
new file mode 100644
index 0000000..4764128
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.NullableValuesItem.Serialization.g.cs
@@ -0,0 +1,496 @@
+//
+// This code was generated by the ModernUO Serialization Generator tool.
+// Version: {VERSION}
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+
+#pragma warning disable
+
+namespace Server.TestContent
+{
+ [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")]
+ public partial class NullableValuesItem
+ {
+ private const int SerializationVersion = 1;
+
+ public int? Count
+ {
+ get => _count;
+ set
+ {
+ if (value != _count)
+ {
+ _count = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public int? Encoded
+ {
+ get => _encoded;
+ set
+ {
+ if (value != _encoded)
+ {
+ _encoded = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public Server.TestContent.Mood? Mood
+ {
+ get => _mood;
+ set
+ {
+ if (value != _mood)
+ {
+ _mood = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public Server.Point3D? Location
+ {
+ get => _location;
+ set
+ {
+ if (value != _location)
+ {
+ _location = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public System.TimeSpan? Cooldown
+ {
+ get => _cooldown;
+ set
+ {
+ if (value != _cooldown)
+ {
+ _cooldown = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public Server.TestContent.Stats? Stats
+ {
+ get => _stats;
+ set
+ {
+ if (!System.Collections.Generic.EqualityComparer.Default.Equals(value, _stats))
+ {
+ _stats = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public System.Collections.Generic.List Samples
+ {
+ get => _samples;
+ set
+ {
+ if (value != _samples)
+ {
+ _samples = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public void AddToSamples(int? value)
+ {
+ _samples ??= new System.Collections.Generic.List();
+ _samples.Add(value);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+
+ public void RemoveFromSamples(int? value)
+ {
+ if (_samples?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public void InsertIntoSamples(int index, int? value)
+ {
+ _samples ??= new System.Collections.Generic.List();
+ _samples.Insert(index, value);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+
+ public void RemoveFromSamplesAt(int index)
+ {
+ if (_samples != null)
+ {
+ _samples.RemoveAt(index);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public void ClearSamples()
+ {
+ if (_samples?.Count > 0)
+ {
+ _samples.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public System.Collections.Generic.Dictionary Weights
+ {
+ get => _weights;
+ set
+ {
+ if (value != _weights)
+ {
+ _weights = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public void AddToWeights(int key, double? value)
+ {
+ _weights ??= new System.Collections.Generic.Dictionary();
+ if (_weights.TryAdd(key, value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public void RemoveFromWeights(int key)
+ {
+ if (_weights?.Remove(key) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public void ReplaceInWeights(int key, double? value)
+ {
+ _weights ??= new System.Collections.Generic.Dictionary();
+ _weights[key] = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+
+ public void ClearWeights()
+ {
+ if (_weights?.Count > 0)
+ {
+ _weights.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+
+ public int? Bonus
+ {
+ get => _bonus;
+ set
+ {
+ if (value != _bonus)
+ {
+ _bonus = value;
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
+ }
+ }
+
+ public NullableValuesItem(Server.Serial serial)
+ {
+ Serial = serial;
+ }
+
+ ref struct V0Content
+ {
+ internal readonly int? Count;
+ internal readonly int? Bonus;
+
+ [System.Flags]
+ private enum V0SaveFlag
+ {
+ None = 0x00000000,
+ Bonus = 0x00000001,
+ }
+ internal V0Content(Server.IGenericReader reader, Server.TestContent.NullableValuesItem entity)
+ {
+ var saveFlags = reader.ReadEnum();
+ if (reader.ReadBool())
+ {
+ int CountValue;
+ CountValue = reader.ReadInt();
+ Count = CountValue;
+ }
+ else
+ {
+ Count = null;
+ }
+
+ if ((saveFlags & V0SaveFlag.Bonus) != 0)
+ {
+ if (reader.ReadBool())
+ {
+ int BonusValue;
+ BonusValue = reader.ReadInt();
+ Bonus = BonusValue;
+ }
+ else
+ {
+ Bonus = null;
+ }
+ }
+ else
+ {
+ Bonus = default;
+ }
+ }
+ }
+
+ public virtual void Serialize(Server.IGenericWriter writer)
+ {
+ writer.WriteEncodedInt(SerializationVersion);
+
+ var saveFlags = SaveFlag.None;
+ if (ShouldSerializeBonus())
+ {
+ saveFlags |= SaveFlag.Bonus;
+ }
+ writer.WriteEnum(saveFlags);
+
+ writer.Write(_count.HasValue);
+ if (_count.HasValue)
+ {
+ var _countValue = _count.Value;
+ writer.Write(_countValue);
+ }
+
+ writer.Write(_encoded.HasValue);
+ if (_encoded.HasValue)
+ {
+ var _encodedValue = _encoded.Value;
+ writer.WriteEncodedInt(_encodedValue);
+ }
+
+ writer.Write(_mood.HasValue);
+ if (_mood.HasValue)
+ {
+ var _moodValue = _mood.Value;
+ writer.WriteEnum(_moodValue);
+ }
+
+ writer.Write(_location.HasValue);
+ if (_location.HasValue)
+ {
+ var _locationValue = _location.Value;
+ writer.Write(_locationValue);
+ }
+
+ writer.Write(_cooldown.HasValue);
+ if (_cooldown.HasValue)
+ {
+ var _cooldownValue = _cooldown.Value;
+ writer.Write(_cooldownValue);
+ }
+
+ writer.Write(_stats.HasValue);
+ if (_stats.HasValue)
+ {
+ var _statsValue = _stats.Value;
+ _statsValue.Serialize(writer);
+ }
+
+ var _samplesCount = _samples?.Count ?? 0;
+ writer.WriteEncodedInt(_samplesCount);
+ if (_samplesCount > 0)
+ {
+ foreach (var _samplesEntry in _samples!)
+ {
+ writer.Write(_samplesEntry.HasValue);
+ if (_samplesEntry.HasValue)
+ {
+ var _samplesEntryValue = _samplesEntry.Value;
+ writer.Write(_samplesEntryValue);
+ }
+ }
+ }
+
+ var _weightsCount = _weights?.Count ?? 0;
+ writer.WriteEncodedInt(_weightsCount);
+ if (_weightsCount > 0)
+ {
+ foreach (var (_weightsKey, _weightsValue) in _weights!)
+ {
+ writer.Write(_weightsKey);
+ writer.Write(_weightsValue.HasValue);
+ if (_weightsValue.HasValue)
+ {
+ var _weightsValueValue = _weightsValue.Value;
+ writer.Write(_weightsValueValue);
+ }
+ }
+ }
+
+ if ((saveFlags & SaveFlag.Bonus) != 0)
+ {
+ writer.Write(_bonus.HasValue);
+ if (_bonus.HasValue)
+ {
+ var _bonusValue = _bonus.Value;
+ writer.Write(_bonusValue);
+ }
+ }
+ }
+
+ public virtual void Deserialize(Server.IGenericReader reader)
+ {
+ var version = reader.ReadEncodedInt();
+
+ if (version == 0)
+ {
+ MigrateFrom(new V0Content(reader, this));
+ Server.ISerializableExtensions.MarkDirty(this);
+ return;
+ }
+
+ var saveFlags = reader.ReadEnum();
+
+ if (reader.ReadBool())
+ {
+ int _countValue;
+ _countValue = reader.ReadInt();
+ _count = _countValue;
+ }
+ else
+ {
+ _count = null;
+ }
+
+ if (reader.ReadBool())
+ {
+ int _encodedValue;
+ _encodedValue = reader.ReadEncodedInt();
+ _encoded = _encodedValue;
+ }
+ else
+ {
+ _encoded = null;
+ }
+
+ if (reader.ReadBool())
+ {
+ Server.TestContent.Mood _moodValue;
+ _moodValue = reader.ReadEnum();
+ _mood = _moodValue;
+ }
+ else
+ {
+ _mood = null;
+ }
+
+ if (reader.ReadBool())
+ {
+ Server.Point3D _locationValue;
+ _locationValue = reader.ReadPoint3D();
+ _location = _locationValue;
+ }
+ else
+ {
+ _location = null;
+ }
+
+ if (reader.ReadBool())
+ {
+ System.TimeSpan _cooldownValue;
+ _cooldownValue = reader.ReadTimeSpan();
+ _cooldown = _cooldownValue;
+ }
+ else
+ {
+ _cooldown = null;
+ }
+
+ if (reader.ReadBool())
+ {
+ Server.TestContent.Stats _statsValue;
+ _statsValue = new Server.TestContent.Stats();
+ _statsValue.Deserialize(reader);
+ _stats = _statsValue;
+ }
+ else
+ {
+ _stats = null;
+ }
+
+ int? _samplesEntry;
+ var _samplesCount = reader.ReadEncodedInt();
+ _samples = new System.Collections.Generic.List(_samplesCount);
+ for (var _samplesIndex = 0; _samplesIndex < _samplesCount; _samplesIndex++)
+ {
+ if (reader.ReadBool())
+ {
+ int _samplesEntryValue;
+ _samplesEntryValue = reader.ReadInt();
+ _samplesEntry = _samplesEntryValue;
+ }
+ else
+ {
+ _samplesEntry = null;
+ }
+ _samples.Add(_samplesEntry);
+ }
+
+ int _weightsKey;
+ double? _weightsValue;
+ var _weightsCount = reader.ReadEncodedInt();
+ _weights = new System.Collections.Generic.Dictionary(_weightsCount);
+ for (var _weightsIndex = 0; _weightsIndex < _weightsCount; _weightsIndex++)
+ {
+ _weightsKey = reader.ReadInt();
+ if (reader.ReadBool())
+ {
+ double _weightsValueValue;
+ _weightsValueValue = reader.ReadDouble();
+ _weightsValue = _weightsValueValue;
+ }
+ else
+ {
+ _weightsValue = null;
+ }
+ if (typeof(int).IsValueType || _weightsKey != default)
+ {
+ _weights.Add(_weightsKey, _weightsValue);
+ }
+ }
+
+ if ((saveFlags & SaveFlag.Bonus) != 0)
+ {
+ if (reader.ReadBool())
+ {
+ int _bonusValue;
+ _bonusValue = reader.ReadInt();
+ _bonus = _bonusValue;
+ }
+ else
+ {
+ _bonus = null;
+ }
+ }
+ }
+
+ [System.Flags]
+ private enum SaveFlag
+ {
+ None = 0x00000000,
+ Bonus = 0x00000001,
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.Stats.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.Stats.Serialization.g.cs
new file mode 100644
index 0000000..a0a1706
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.Stats.Serialization.g.cs
@@ -0,0 +1,44 @@
+//
+// This code was generated by the ModernUO Serialization Generator tool.
+// Version: {VERSION}
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+
+#pragma warning disable
+
+namespace Server.TestContent
+{
+ [System.CodeDom.Compiler.GeneratedCode("ModernUO.Serialization.Generator", "{VERSION}")]
+ public partial struct Stats
+ {
+ private const int SerializationVersion = 0;
+
+ public int Strength
+ {
+ get => _strength;
+ set
+ {
+ if (value != _strength)
+ {
+ _strength = value;
+ }
+ }
+ }
+
+ public void Serialize(Server.IGenericWriter writer)
+ {
+ writer.WriteEncodedInt(SerializationVersion);
+
+ writer.Write(_strength);
+ }
+
+ public void Deserialize(Server.IGenericReader reader)
+ {
+ var version = reader.ReadEncodedInt();
+
+ _strength = reader.ReadInt();
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Input.cs
new file mode 100644
index 0000000..b04241d
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Input.cs
@@ -0,0 +1,67 @@
+using System;
+using System.Collections.Generic;
+using ModernUO.Serialization;
+using Server;
+
+namespace Server.TestContent
+{
+ public enum Mood
+ {
+ Calm,
+ Angry
+ }
+
+ [SerializationGenerator(0)]
+ public partial struct Stats
+ {
+ [SerializableField(0)]
+ private int _strength;
+ }
+
+ // Nullable fields: a HasValue bool, then T through its own rule.
+ [SerializationGenerator(1)]
+ public partial class NullableValuesItem : ISerializable
+ {
+ [SerializableField(0)]
+ private int? _count;
+
+ [SerializableField(1)]
+ [EncodedInt]
+ private int? _encoded;
+
+ [SerializableField(2)]
+ private Mood? _mood;
+
+ [SerializableField(3)]
+ private Point3D? _location;
+
+ [SerializableField(4)]
+ private TimeSpan? _cooldown;
+
+ [SerializableField(5)]
+ private Stats? _stats;
+
+ [SerializableField(6)]
+ private List _samples;
+
+ [SerializableField(7)]
+ private Dictionary _weights;
+
+ [SerializableField(8)]
+ [SaveFlag(nameof(ShouldSerializeBonus))]
+ private int? _bonus;
+
+ private bool ShouldSerializeBonus() => _bonus != null;
+
+ public DateTime Created { get; set; }
+ public Serial Serial { get; }
+ public bool Deleted => false;
+ public void Delete() { }
+
+ private void MigrateFrom(V0Content content)
+ {
+ _count = content.Count;
+ _bonus = content.Bonus;
+ }
+ }
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Server.TestContent.NullableValuesItem.v0.json b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Server.TestContent.NullableValuesItem.v0.json
new file mode 100644
index 0000000..8061225
--- /dev/null
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Server.TestContent.NullableValuesItem.v0.json
@@ -0,0 +1,27 @@
+{
+ "version": 0,
+ "type": "Server.TestContent.NullableValuesItem",
+ "properties": [
+ {
+ "name": "Count",
+ "type": "int?",
+ "rule": "NullableMigrationRule",
+ "ruleArguments": [
+ "int",
+ "PrimitiveTypeMigrationRule",
+ ""
+ ]
+ },
+ {
+ "name": "Bonus",
+ "type": "int?",
+ "usesSaveFlag": true,
+ "rule": "NullableMigrationRule",
+ "ruleArguments": [
+ "int",
+ "PrimitiveTypeMigrationRule",
+ ""
+ ]
+ }
+ ]
+}
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/ParentCtorPreferred/Expected/Server.TestContent.Roster.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/ParentCtorPreferred/Expected/Server.TestContent.Roster.Serialization.g.cs
index b03d4cc..5e4940d 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/ParentCtorPreferred/Expected/Server.TestContent.Roster.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/ParentCtorPreferred/Expected/Server.TestContent.Roster.Serialization.g.cs
@@ -30,32 +30,42 @@ public System.Collections.Generic.List Entries
public void AddToEntries(Server.TestContent.RosterEntry value)
{
- Entries.Add(value);
+ _entries ??= new System.Collections.Generic.List();
+ _entries.Add(value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromEntries(Server.TestContent.RosterEntry value)
{
- Entries.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void InsertIntoEntries(int index, Server.TestContent.RosterEntry value)
{
- Entries.Insert(index, value);
+ _entries ??= new System.Collections.Generic.List();
+ _entries.Insert(index, value);
Server.ISerializableExtensions.MarkDirty(this);
}
public void RemoveFromEntriesAt(int index)
{
- Entries.RemoveAt(index);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries != null)
+ {
+ _entries.RemoveAt(index);
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ClearEntries()
{
- Entries.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_entries?.Count > 0)
+ {
+ _entries.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public System.Collections.Generic.Dictionary ById
@@ -73,26 +83,35 @@ public void ClearEntries()
public void AddToById(int key, Server.TestContent.RosterEntry value)
{
- ById.Add(key, value);
- Server.ISerializableExtensions.MarkDirty(this);
+ _byId ??= new System.Collections.Generic.Dictionary();
+ if (_byId.TryAdd(key, value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void RemoveFromById(int key)
{
- ById.Remove(key);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_byId?.Remove(key) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void ReplaceInById(int key, Server.TestContent.RosterEntry value)
{
- ById[key] = value;
+ _byId ??= new System.Collections.Generic.Dictionary();
+ _byId[key] = value;
Server.ISerializableExtensions.MarkDirty(this);
}
public void ClearById()
{
- ById.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_byId?.Count > 0)
+ {
+ _byId.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public Server.TestContent.RosterEntry Leader
diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SortedSetWithComparer/Expected/Server.TestContent.SortedSetItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SortedSetWithComparer/Expected/Server.TestContent.SortedSetItem.Serialization.g.cs
index 329f904..1b77384 100644
--- a/ModernUO.Serialization.Generator.Tests/Snapshots/SortedSetWithComparer/Expected/Server.TestContent.SortedSetItem.Serialization.g.cs
+++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SortedSetWithComparer/Expected/Server.TestContent.SortedSetItem.Serialization.g.cs
@@ -30,21 +30,28 @@ public System.Collections.Generic.SortedSet Names
public void AddToNames(string value)
{
- Names.Add(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ _names ??= new System.Collections.Generic.SortedSet(new Server.TestContent.CaseInsensitiveComparer());
+ if (_names.Add(value))
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public void RemoveFromNames(string value)
{
- Names.Remove(value);
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_names?.Remove(value) == true)
+ {
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
-
public void ClearNames()
{
- Names.Clear();
- Server.ISerializableExtensions.MarkDirty(this);
+ if (_names?.Count > 0)
+ {
+ _names.Clear();
+ Server.ISerializableExtensions.MarkDirty(this);
+ }
}
public SortedSetItem(Server.Serial serial)
diff --git a/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj b/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj
index f3f8ca2..d09c2d1 100755
--- a/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj
+++ b/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj
@@ -4,8 +4,8 @@
ModernUO.Serialization.Generator
netstandard2.0
preview
- 4.1.1
- 4.1.1
+ 4.2.0
+ 4.2.0
ModernUO.Serialization.Generator
true
false
diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs
index 9a26f13..2477595 100644
--- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs
+++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs
@@ -429,8 +429,11 @@ static bool IsAllowChangeShape(IMethodSymbol method, ITypeSymbol fieldType) =>
var dsIsDictionary = false;
var dsIsList = false;
var dsIsCollection = false;
+ var dsIsSet = false;
+ var dsHasTryAdd = false;
string? dsElementType = null;
string? dsValueType = null;
+ string? dsCreateExpression = null;
if (!fieldSymbol.IsReadOnly && elementType != null)
{
@@ -440,6 +443,14 @@ static bool IsAllowChangeShape(IMethodSymbol method, ITypeSymbol fieldType) =>
dsIsCollection = propertyType.IsCollection(compilation);
dsElementType = elementType.ToString();
dsValueType = dsIsDictionary ? namedTypeSymbol!.TypeArguments[1].ToString() : null;
+ dsIsSet = propertyType.IsSet(compilation);
+ dsHasTryAdd = propertyType.IsDictionary(compilation);
+ dsCreateExpression = GetDataStructureCreateExpression(
+ compilation,
+ classSymbol,
+ namedTypeSymbol,
+ allAttributes
+ );
}
fieldEmissions.Add(
@@ -461,8 +472,11 @@ static bool IsAllowChangeShape(IMethodSymbol method, ITypeSymbol fieldType) =>
dsIsDictionary,
dsIsList,
dsIsCollection,
+ dsIsSet,
+ dsHasTryAdd,
dsElementType,
- dsValueType
+ dsValueType,
+ dsCreateExpression
)
);
@@ -669,4 +683,37 @@ private static bool HasGeneratedMutation(List fieldEmissions
return false;
}
+
+ // How AddToX/InsertIntoX/ReplaceInX lazily create a null collection. Null when the type cannot be
+ // constructed here (interface, abstract, no accessible parameterless ctor); those keep requiring
+ // the collection to exist. A [SortedSetComparer] is passed through, as deserialization does.
+ private static string? GetDataStructureCreateExpression(
+ Compilation compilation,
+ INamedTypeSymbol classSymbol,
+ INamedTypeSymbol? type,
+ ImmutableArray attributes
+ )
+ {
+ if (type is not { TypeKind: TypeKind.Class, IsAbstract: false })
+ {
+ return null;
+ }
+
+ var typeName = type.ToSerializedTypeName();
+
+ if (type.IsSortedSet(compilation) && attributes.TryGetSortedSetComparer(compilation, out var comparer))
+ {
+ return $"new {typeName}({comparer})";
+ }
+
+ foreach (var ctor in type.InstanceConstructors)
+ {
+ if (ctor.Parameters.Length == 0 && compilation.IsSymbolAccessibleWithin(ctor, classSymbol))
+ {
+ return $"new {typeName}()";
+ }
+ }
+
+ return null;
+ }
}
diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs
index 4918bc4..262086b 100644
--- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs
+++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.DataStructure.cs
@@ -13,6 +13,31 @@ private static void AppendMarkDirty(this StringBuilder source, string indent, st
}
}
+ private static void AppendLazyCreate(this StringBuilder source, string indent, FieldPropertyModel field)
+ {
+ if (field.DsCreateExpression != null)
+ {
+ source.AppendLine($"{indent} {field.FieldName} ??= {field.DsCreateExpression};");
+ }
+ }
+
+ // Marks dirty only when the mutation reports a change. Without a dirty target, just mutates.
+ private static void AppendIfChanged(
+ this StringBuilder source, string indent, string changed, string? markDirtyMethod, string? statement = null
+ )
+ {
+ if (markDirtyMethod == null)
+ {
+ source.AppendLine($"{indent} {statement ?? changed};");
+ return;
+ }
+
+ source.AppendLine($"{indent} if ({changed})");
+ source.AppendLine($"{indent} {{");
+ source.AppendLine($"{indent} {markDirtyMethod};");
+ source.AppendLine($"{indent} }}");
+ }
+
public static bool GenerateDataStructureMethods(
this StringBuilder source,
string indent,
@@ -28,8 +53,12 @@ public static bool GenerateDataStructureMethods(
}
var propertyName = field.PropertyName;
+ var fieldName = field.FieldName;
var elementTypeName = field.DsElementType;
+ // Mutations go through the backing field: lazily creating the collection through the
+ // property setter would run fieldChanged/allowFieldChange for what is not a replacement.
+ // Removals and clears tolerate a null collection, and only mark dirty on an actual change.
if (field.DsIsDictionary)
{
var valueTypeName = field.DsValueType;
@@ -37,8 +66,19 @@ public static bool GenerateDataStructureMethods(
// Add
source.AppendLine($"{indent}{propertyAccessor} void AddTo{propertyName}({elementTypeName} key, {valueTypeName} value)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Add(key, value);");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendLazyCreate(indent, field);
+ if (field.DsHasTryAdd)
+ {
+ source.AppendIfChanged(indent, $"{fieldName}.TryAdd(key, value)", markDirtyMethod);
+ }
+ else
+ {
+ source.AppendLine($"{indent} if (!{fieldName}.ContainsKey(key))");
+ source.AppendLine($"{indent} {{");
+ source.AppendLine($"{indent} {fieldName}.Add(key, value);");
+ source.AppendMarkDirty($"{indent} ", markDirtyMethod);
+ source.AppendLine($"{indent} }}");
+ }
source.AppendLine($"{indent}}}");
source.AppendLine();
@@ -46,8 +86,7 @@ public static bool GenerateDataStructureMethods(
// Remove
source.AppendLine($"{indent}{propertyAccessor} void RemoveFrom{propertyName}({elementTypeName} key)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Remove(key);");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendIfChanged(indent, $"{fieldName}?.Remove(key) == true", markDirtyMethod, $"{fieldName}?.Remove(key)");
source.AppendLine($"{indent}}}");
source.AppendLine();
@@ -55,7 +94,8 @@ public static bool GenerateDataStructureMethods(
// Replace
source.AppendLine($"{indent}{propertyAccessor} void ReplaceIn{propertyName}({elementTypeName} key, {valueTypeName} value)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}[key] = value;");
+ source.AppendLazyCreate(indent, field);
+ source.AppendLine($"{indent} {fieldName}[key] = value;");
source.AppendMarkDirty(indent, markDirtyMethod);
source.AppendLine($"{indent}}}");
}
@@ -64,8 +104,16 @@ public static bool GenerateDataStructureMethods(
// Add
source.AppendLine($"{indent}{propertyAccessor} void AddTo{propertyName}({elementTypeName} value)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Add(value);");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendLazyCreate(indent, field);
+ if (field.DsIsSet)
+ {
+ source.AppendIfChanged(indent, $"{fieldName}.Add(value)", markDirtyMethod);
+ }
+ else
+ {
+ source.AppendLine($"{indent} {fieldName}.Add(value);");
+ source.AppendMarkDirty(indent, markDirtyMethod);
+ }
source.AppendLine($"{indent}}}");
source.AppendLine();
@@ -73,19 +121,19 @@ public static bool GenerateDataStructureMethods(
// Remove
source.AppendLine($"{indent}{propertyAccessor} void RemoveFrom{propertyName}({elementTypeName} value)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Remove(value);");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendIfChanged(indent, $"{fieldName}?.Remove(value) == true", markDirtyMethod, $"{fieldName}?.Remove(value)");
source.AppendLine($"{indent}}}");
-
- source.AppendLine();
}
if (field.DsIsList)
{
+ source.AppendLine();
+
// Insert
source.AppendLine($"{indent}{propertyAccessor} void InsertInto{propertyName}(int index, {elementTypeName} value)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Insert(index, value);");
+ source.AppendLazyCreate(indent, field);
+ source.AppendLine($"{indent} {fieldName}.Insert(index, value);");
source.AppendMarkDirty(indent, markDirtyMethod);
source.AppendLine($"{indent}}}");
@@ -94,8 +142,11 @@ public static bool GenerateDataStructureMethods(
// RemoveAt
source.AppendLine($"{indent}{propertyAccessor} void RemoveFrom{propertyName}At(int index)");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.RemoveAt(index);");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendLine($"{indent} if ({fieldName} != null)");
+ source.AppendLine($"{indent} {{");
+ source.AppendLine($"{indent} {fieldName}.RemoveAt(index);");
+ source.AppendMarkDirty($"{indent} ", markDirtyMethod);
+ source.AppendLine($"{indent} }}");
source.AppendLine($"{indent}}}");
}
@@ -115,8 +166,11 @@ public static bool GenerateDataStructureMethods(
// Clear
source.AppendLine($"{indent}{propertyAccessor} void Clear{propertyName}()");
source.AppendLine($"{indent}{{");
- source.AppendLine($"{indent} {propertyName}.Clear();");
- source.AppendMarkDirty(indent, markDirtyMethod);
+ source.AppendLine($"{indent} if ({fieldName}?.Count > 0)");
+ source.AppendLine($"{indent} {{");
+ source.AppendLine($"{indent} {fieldName}.Clear();");
+ source.AppendMarkDirty($"{indent} ", markDirtyMethod);
+ source.AppendLine($"{indent} }}");
source.AppendLine($"{indent}}}");
}
diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs
index f381280..942216f 100644
--- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs
+++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationEntityGeneration.ContentStruct.cs
@@ -126,7 +126,11 @@ string classDisplayString
);
source.AppendLine($"{innerIndent}}}\n{innerIndent}else\n{innerIndent}{{");
- source.AppendLine($"{innerIndent} {property.Name} = default;");
+ SerializableMigrationRulesEngine.Rules[property.Rule].GenerateMigrationAbsentAssignment(
+ source,
+ $"{innerIndent} ",
+ property
+ );
source.AppendLine($"{innerIndent}}}");
}
}
diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs
index 77b44dc..cb21a56 100644
--- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs
+++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs
@@ -135,8 +135,11 @@ public sealed record FieldPropertyModel(
bool DsIsDictionary,
bool DsIsList,
bool DsIsCollection,
+ bool DsIsSet,
+ bool DsHasTryAdd,
string? DsElementType,
- string? DsValueType
+ string? DsValueType,
+ string? DsCreateExpression
)
{
public bool HasDataStructureMethods => DsIsArray || DsIsDictionary || DsIsList || DsIsCollection;
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/ISerializableMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/ISerializableMigrationRule.cs
index 2fffbcf..2db54bc 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/ISerializableMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/ISerializableMigrationRule.cs
@@ -29,6 +29,13 @@ void GenerateMigrationProperty(
SerializableProperty property
);
+ // Assigns the content members declared by GenerateMigrationProperty when a save flag is absent.
+ void GenerateMigrationAbsentAssignment(
+ StringBuilder source,
+ string indent,
+ SerializableProperty property
+ );
+
bool GenerateRuleState(
Compilation compilation,
ISymbol symbol,
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/ArrayMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/ArrayMigrationRule.cs
index 87d4272..c34d189 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/ArrayMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/ArrayMigrationRule.cs
@@ -58,7 +58,7 @@ out string[] ruleArguments
ruleArguments[index++] = "@CanBeNull";
}
- ruleArguments[index++] = arrayTypeSymbol.ElementType.ToDisplayString();
+ ruleArguments[index++] = arrayTypeSymbol.ElementType.ToSerializedTypeName();
ruleArguments[index++] = serializableArrayType.Rule;
if (length > 0)
{
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/DictionaryMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/DictionaryMigrationRule.cs
index 5162f20..baa399a 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/DictionaryMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/DictionaryMigrationRule.cs
@@ -82,7 +82,7 @@ out string[] ruleArguments
ruleArguments[index++] = "@CanBeNull";
}
- ruleArguments[index++] = keySymbolType.ToDisplayString();
+ ruleArguments[index++] = keySymbolType.ToSerializedTypeName();
ruleArguments[index++] = serializableKeyProperty.Rule;
ruleArguments[index++] = keyArgumentsLength.ToString();
@@ -92,7 +92,7 @@ out string[] ruleArguments
index += keyArgumentsLength;
}
- ruleArguments[index++] = valueSymbolType.ToDisplayString();
+ ruleArguments[index++] = valueSymbolType.ToSerializedTypeName();
ruleArguments[index++] = serializableValueProperty.Rule;
ruleArguments[index++] = valueArgumentsLength.ToString();
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/HashSetMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/HashSetMigrationRule.cs
index c627054..b8c4e57 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/HashSetMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/HashSetMigrationRule.cs
@@ -68,7 +68,7 @@ out string[] ruleArguments
{
ruleArguments[index++] = "@CanBeNull";
}
- ruleArguments[index++] = setTypeSymbol.ToDisplayString();
+ ruleArguments[index++] = setTypeSymbol.ToSerializedTypeName();
ruleArguments[index++] = serializableSetType.Rule;
if (length > 0)
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
index 47f0b5d..ca7862f 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/KeyValuePairMigrationRule.cs
@@ -70,7 +70,7 @@ out string[] ruleArguments
var index = 0;
// Key
- ruleArguments[index++] = keySymbolType.ToDisplayString();
+ ruleArguments[index++] = keySymbolType.ToSerializedTypeName();
ruleArguments[index++] = keySerializedProperty.Rule;
ruleArguments[index++] = keyArgumentsLength.ToString();
if (keyArgumentsLength > 0)
@@ -80,7 +80,7 @@ out string[] ruleArguments
}
// Value
- ruleArguments[index++] = valueSymbolType.ToDisplayString();
+ ruleArguments[index++] = valueSymbolType.ToSerializedTypeName();
ruleArguments[index++] = valueSerializedProperty.Rule;
ruleArguments[index++] = valueArgumentsLength.ToString();
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/ListMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/ListMigrationRule.cs
index 4303fbf..2d0cb6d 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/ListMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/ListMigrationRule.cs
@@ -68,7 +68,7 @@ out string[] ruleArguments
{
ruleArguments[index++] = "@CanBeNull";
}
- ruleArguments[index++] = listTypeSymbol.ToDisplayString();
+ ruleArguments[index++] = listTypeSymbol.ToSerializedTypeName();
ruleArguments[index++] = serializableListType.Rule;
if (length > 0)
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/MigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/MigrationRule.cs
index 1c4cf8d..c677fef 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/MigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/MigrationRule.cs
@@ -35,6 +35,13 @@ public virtual void GenerateMigrationProperty(
source.AppendLine($"{indent}internal readonly {type} {property.FieldName ?? property.Name};");
}
+ public virtual void GenerateMigrationAbsentAssignment(
+ StringBuilder source, string indent, SerializableProperty property
+ )
+ {
+ source.AppendLine($"{indent}{property.Name} = default;");
+ }
+
public abstract bool GenerateRuleState(
Compilation compilation, ISymbol symbol, ImmutableArray attributes,
ISymbol? parentSymbol, out string[] ruleArguments
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/NullableMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/NullableMigrationRule.cs
new file mode 100644
index 0000000..a13cca8
--- /dev/null
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/NullableMigrationRule.cs
@@ -0,0 +1,140 @@
+/*************************************************************************
+ * ModernUO *
+ * Copyright 2019-2026 - ModernUO Development Team *
+ * Email: hi@modernuo.com *
+ * File: NullableMigrationRule.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 . *
+ *************************************************************************/
+
+using System;
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+
+namespace ModernUO.Serialization.Generator;
+
+///
+/// Nullable<T> value types: a HasValue bool, then the value through T's own rule when present.
+/// Rule arguments are [T, T's rule, ...T's rule arguments].
+///
+public class NullableMigrationRule : MigrationRule
+{
+ public override string RuleName => nameof(NullableMigrationRule);
+
+ public override bool GenerateRuleState(
+ Compilation compilation,
+ ISymbol symbol,
+ ImmutableArray attributes,
+ ISymbol? parentSymbol,
+ out string[] ruleArguments
+ )
+ {
+ if (symbol is not INamedTypeSymbol { OriginalDefinition.SpecialType: SpecialType.System_Nullable_T } nullable)
+ {
+ ruleArguments = null;
+ return false;
+ }
+
+ var valueType = nullable.TypeArguments[0];
+
+ var serializableValue = SerializableMigrationRulesEngine.GenerateSerializableProperty(
+ compilation,
+ "NullableValue",
+ valueType,
+ 0,
+ attributes,
+ parentSymbol,
+ null
+ );
+
+ var length = serializableValue.RuleArguments?.Length ?? 0;
+ ruleArguments = new string[2 + length];
+ ruleArguments[0] = valueType.ToSerializedTypeName();
+ ruleArguments[1] = serializableValue.Rule;
+
+ if (length > 0)
+ {
+ Array.Copy(serializableValue.RuleArguments!, 0, ruleArguments, 2, length);
+ }
+
+ return true;
+ }
+
+ public override void GenerateDeserializationMethod(
+ StringBuilder source,
+ string indent,
+ SerializableProperty property,
+ string? parentReference,
+ bool isMigration = false
+ )
+ {
+ var expectedRule = RuleName;
+ var ruleName = property.Rule;
+ if (expectedRule != ruleName)
+ {
+ throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}.");
+ }
+
+ var propertyName = property.FieldName ?? property.Name;
+ var valueName = $"{propertyName}Value";
+ var valueProperty = GetValueProperty(property, valueName, out var valueRule);
+
+ source.AppendLine($"{indent}if (reader.ReadBool())");
+ source.AppendLine($"{indent}{{");
+ source.AppendLine($"{indent} {valueProperty.Type} {valueName};");
+ valueRule.GenerateDeserializationMethod(source, $"{indent} ", valueProperty, parentReference);
+ source.AppendLine($"{indent} {propertyName} = {valueName};");
+ source.AppendLine($"{indent}}}");
+ source.AppendLine($"{indent}else");
+ source.AppendLine($"{indent}{{");
+ source.AppendLine($"{indent} {propertyName} = null;");
+ source.AppendLine($"{indent}}}");
+ }
+
+ public override void GenerateSerializationMethod(StringBuilder source, string indent, SerializableProperty property)
+ {
+ var expectedRule = RuleName;
+ var ruleName = property.Rule;
+ if (expectedRule != ruleName)
+ {
+ throw new ArgumentException($"Invalid rule applied to property {ruleName}. Expecting {expectedRule}, but received {ruleName}.");
+ }
+
+ var propertyName = property.FieldName ?? property.Name;
+ var valueName = $"{propertyName}Value";
+ var valueProperty = GetValueProperty(property, valueName, out var valueRule);
+
+ source.AppendLine($"{indent}writer.Write({propertyName}.HasValue);");
+ source.AppendLine($"{indent}if ({propertyName}.HasValue)");
+ source.AppendLine($"{indent}{{");
+ source.AppendLine($"{indent} var {valueName} = {propertyName}.Value;");
+ valueRule.GenerateSerializationMethod(source, $"{indent} ", valueProperty);
+ source.AppendLine($"{indent}}}");
+ }
+
+ private static SerializableProperty GetValueProperty(
+ SerializableProperty property, string valueName, out ISerializableMigrationRule valueRule
+ )
+ {
+ var ruleArguments = property.RuleArguments!;
+ valueRule = SerializableMigrationRulesEngine.Rules[ruleArguments[1]];
+
+ var valueRuleArguments = new string[ruleArguments.Length - 2];
+ Array.Copy(ruleArguments, 2, valueRuleArguments, 0, valueRuleArguments.Length);
+
+ return new SerializableProperty
+ {
+ Name = valueName,
+ Type = ruleArguments[0],
+ Rule = valueRule.RuleName,
+ RuleArguments = valueRuleArguments.Length > 0 ? valueRuleArguments : null
+ };
+ }
+}
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/SortedSetMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/SortedSetMigrationRule.cs
index bc63131..baa6f93 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/SortedSetMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/SortedSetMigrationRule.cs
@@ -75,7 +75,7 @@ out string[] ruleArguments
ruleArguments[index++] = $"@Comparer:{comparerExpression}";
}
- ruleArguments[index++] = setTypeSymbol.ToDisplayString();
+ ruleArguments[index++] = setTypeSymbol.ToSerializedTypeName();
ruleArguments[index++] = serializableSetType.Rule;
if (length > 0)
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs
index 63d7e28..2432c3e 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs
@@ -65,6 +65,15 @@ public override void GenerateMigrationProperty(
source.AppendLine($"{indent}internal readonly System.TimeSpan {property.Name}Delay;");
}
+ // Same "no timer was running" sentinels the present branch produces for a MinValue read.
+ public override void GenerateMigrationAbsentAssignment(
+ StringBuilder source, string indent, SerializableProperty property
+ )
+ {
+ source.AppendLine($"{indent}{property.Name}Next = System.DateTime.MinValue;");
+ source.AppendLine($"{indent}{property.Name}Delay = System.TimeSpan.MinValue;");
+ }
+
public override void GenerateDeserializationMethod(
StringBuilder source,
string indent,
diff --git a/ModernUO.Serialization.Generator/SerializableMigration/SerializableMigrationRulesEngine.cs b/ModernUO.Serialization.Generator/SerializableMigration/SerializableMigrationRulesEngine.cs
index 3b0ded9..a1022fd 100644
--- a/ModernUO.Serialization.Generator/SerializableMigration/SerializableMigrationRulesEngine.cs
+++ b/ModernUO.Serialization.Generator/SerializableMigration/SerializableMigrationRulesEngine.cs
@@ -27,6 +27,7 @@ static SerializableMigrationRulesEngine()
{
var rules = new ISerializableMigrationRule[]
{
+ new NullableMigrationRule(),
new EnumMigrationRule(),
new ListMigrationRule(),
new ArrayMigrationRule(),
@@ -71,7 +72,7 @@ out var ruleArguments
return new SerializableProperty
{
Name = propertyName,
- Type = propertyType.ToDisplayString(),
+ Type = propertyType.ToSerializedTypeName(),
Order = order,
UsesSaveFlag = serializableFieldSaveFlagMethods?.DetermineFieldShouldSerialize != null ? true : null,
Rule = rule.RuleName,
@@ -80,6 +81,6 @@ out var ruleArguments
}
}
- throw new NoRuleFoundException(propertyName, propertyType.ToDisplayString());
+ throw new NoRuleFoundException(propertyName, propertyType.ToSerializedTypeName());
}
}
diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
index 3c7d09c..0af11a6 100644
--- a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
+++ b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.Builtin.cs
@@ -34,6 +34,15 @@ public static partial class SymbolMetadata
public const string GUID_STRUCT = "System.Guid";
public const string TYPE_CLASS = "System.Type";
+ // Serialization type names drop reference-type nullable annotations ("string?" reads and writes
+ // exactly like "string"); Nullable still displays as "int?".
+ private static readonly SymbolDisplayFormat SerializedTypeFormat =
+ SymbolDisplayFormat.CSharpErrorMessageFormat.RemoveMiscellaneousOptions(
+ SymbolDisplayMiscellaneousOptions.IncludeNullableReferenceTypeModifier
+ );
+
+ public static string ToSerializedTypeName(this ISymbol symbol) => symbol.ToDisplayString(SerializedTypeFormat);
+
extension(ISymbol symbol)
{
public bool IsGuid(Compilation compilation) =>
diff --git a/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj b/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj
index 5380973..e19d7b6 100755
--- a/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj
+++ b/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj
@@ -6,8 +6,8 @@
x64;arm64
preview
Exe
- 4.1.1
- 4.1.1
+ 4.2.0
+ 4.2.0
true
ModernUOSchemaGenerator
true
diff --git a/README.md b/README.md
index 234ce28..c5783fe 100644
--- a/README.md
+++ b/README.md
@@ -7,8 +7,8 @@ While it is not the most elegant solution (recommendations and contributions are
Add `ModernUO.Serialization.Generator` and `ModernUO.Serialization.Annotations` as package references:
```xml
-
-
+
+
```
@@ -120,6 +120,18 @@ migration file generated by the _Serialization Schema Generator_.
_*WARNING*_: DO NOT ASSIGN A VALUE BACK TO THE ORIGINAL PROPERTY (`_prefix`). Always use the generated setter! There are more advanced techniques to handle edge cases if needed.
Contact us in Discord and ask for help if needed!
+### Collection helpers and nullable fields
+
+A `[SerializableField]` collection (`List`, `HashSet`, `SortedSet`, `Dictionary`) also gets
+`AddToX`, `RemoveFromX` and `ClearX` helpers, plus `InsertIntoX`/`RemoveFromXAt` for lists and `ReplaceInX` for dictionaries.
+They tolerate a null collection: adds create it on first use (a `[SortedSetComparer]` is kept), while removes and clears
+do nothing. They mark the entity dirty only when the collection actually changed, so adding a duplicate to a set or
+dictionary, or removing a missing entry, does not.
+
+Nullable reference annotations (`string?`, `List- ?`) are ignored for serialization. Nullable value types
+(`int?`, `Point3D?`, a nullable `[SerializationGenerator]` struct, or `List`) are written as a `bool` followed by the
+value when present.
+
### Basic Property Serialization
Similar to our previous example, we can do a _less magical_ way of hinting to the code generator that we want to serialize a value.