From b6ef616bc0224e6bb3a42d4caf1146b8c93094d1 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Thu, 24 Sep 2026 00:48:13 -0700 Subject: [PATCH] feat: Nullable fields, null-safe collection helpers, migration timer fix (v4.2.0) - Fix: a migration content struct with a save-flagged Timer no longer emits `{Name} = default;` for an absent flag (CS0120/CS0103). Rules now own the absent assignment (ISerializableMigrationRule.GenerateMigrationAbsentAssignment); TimerMigrationRule sets Next/Delay to DateTime.MinValue/TimeSpan.MinValue. - Fix: nullable reference annotations (string?, Dictionary, string?[]) crashed the generator (SG0001). Serialized type names now drop reference-type `?` and keep Nullable. - Feature: NullableMigrationRule serializes Nullable fields (int?, enums, Point3D?, [SerializationGenerator] structs, and nested in collections) as a HasValue bool followed by the value through T's own rule. - Collection helpers mutate the backing field, lazily create a null collection on add/insert/replace (keeping [SortedSetComparer]), tolerate null on remove/clear, and mark dirty only on an actual change (set Add, dictionary TryAdd, Remove results, non-empty Clear). - Bump Generator, SchemaGenerator and Annotations to 4.2.0. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../ModernUO.Serialization.Annotations.csproj | 4 +- .../DataStructureMethodTests.cs | 98 ++++ .../Helpers/SourceGeneratorTestHelper.cs | 20 +- .../MigrationSaveFlagTests.cs | 144 ++++- .../NullableValueTypeTests.cs | 193 +++++++ ...Content.CollectionsItem.Serialization.g.cs | 70 ++- ...tent.FieldModifiersItem.Serialization.g.cs | 23 +- ...ontent.FlaggedTimerItem.Serialization.g.cs | 124 +++++ .../Snapshots/MigrationSaveFlagTimer/Input.cs | 43 ++ ...erver.TestContent.FlaggedTimerItem.v0.json | 39 ++ ...stContent.ExtendedOwner.Serialization.g.cs | 26 +- ...erver.TestContent.Owner.Serialization.g.cs | 26 +- ...ent.NoDirtyTargetRecord.Serialization.g.cs | 31 +- ...NullableCollectionsItem.Serialization.g.cs | 351 +++++++++++++ .../Snapshots/NullableCollections/Input.cs | 48 ++ ...tent.NullableValuesItem.Serialization.g.cs | 496 ++++++++++++++++++ ...erver.TestContent.Stats.Serialization.g.cs | 44 ++ .../Snapshots/NullableValueTypes/Input.cs | 67 +++ ...ver.TestContent.NullableValuesItem.v0.json | 27 + ...rver.TestContent.Roster.Serialization.g.cs | 49 +- ...stContent.SortedSetItem.Serialization.g.cs | 21 +- .../ModernUO.Serialization.Generator.csproj | 4 +- ...SerializableEntityGeneration.BuildModel.cs | 49 +- ...ializableEntityGeneration.DataStructure.cs | 86 ++- ...alizationEntityGeneration.ContentStruct.cs | 6 +- .../SerializationModel.cs | 5 +- .../ISerializableMigrationRule.cs | 7 + .../Rules/ArrayMigrationRule.cs | 2 +- .../Rules/DictionaryMigrationRule.cs | 4 +- .../Rules/HashSetMigrationRule.cs | 2 +- .../Rules/KeyValuePairMigrationRule.cs | 4 +- .../Rules/ListMigrationRule.cs | 2 +- .../Rules/MigrationRule.cs | 7 + .../Rules/NullableMigrationRule.cs | 140 +++++ .../Rules/SortedSetMigrationRule.cs | 2 +- .../Rules/TimerMigrationRule.cs | 9 + .../SerializableMigrationRulesEngine.cs | 5 +- .../SymbolMetadata/SymbolMetadata.Builtin.cs | 9 + ...ernUO.Serialization.SchemaGenerator.csproj | 4 +- README.md | 16 +- 40 files changed, 2183 insertions(+), 124 deletions(-) create mode 100644 ModernUO.Serialization.Generator.Tests/DataStructureMethodTests.cs create mode 100644 ModernUO.Serialization.Generator.Tests/NullableValueTypeTests.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Expected/Server.TestContent.FlaggedTimerItem.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Input.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/MigrationSaveFlagTimer/Server.TestContent.FlaggedTimerItem.v0.json create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Expected/Server.TestContent.NullableCollectionsItem.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableCollections/Input.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.NullableValuesItem.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Expected/Server.TestContent.Stats.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Input.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/NullableValueTypes/Server.TestContent.NullableValuesItem.v0.json create mode 100644 ModernUO.Serialization.Generator/SerializableMigration/Rules/NullableMigrationRule.cs 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.