From 874304e9c73c7e9bfc587bcc7cfad0b74d9516cb Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:40:49 -0700 Subject: [PATCH 1/5] feat!: name-based linkage, field-side declarations, and anchored timers (v4) Order-based linkage between serializable fields and their companion methods is removed. Method-side attributes take nameof(_field); every linkage can alternatively be declared on the field itself with the new [SaveFlag], [FieldChanged], and [DeserializeTimer] attributes, and both spellings generate identical code. Timers consolidate into [DeserializeTimer(nameof(Method))] on the timer field, replacing [TimerDrift] and [DeserializeTimerField]. Drift is the default and now stores the timer's next tick as anchored time, so downtime does not consume the remaining delay; wallClock: true keeps absolute deadlines with the unchanged wire format. The restart method is invoked only when a timer was running at save - no more TimeSpan.MinValue sentinel. The legacy "@TimerDrift" delta-time read survives permanently for old-version migration schemas, proven by a fixture whose v0 schema replays ReadDeltaTime while the current version reads anchored time. Removed attributes remain as error-[Obsolete] shells whose messages are the migration instructions. New diagnostics: SG3014 (unknown field reference), SG3015 (linked method missing or wrong signature - save flag, default, and changed shapes were previously never validated and produced broken output on mistakes), SG3016 (both linkage styles on one field), SG3017 (default value without a save flag, previously silently ignored). SG3008 now points at the field-side declaration. Also fixes the README install snippet, which named a nonexistent package id at a stale version, and documents the v4 patterns and migration. Back-to-back benchmarks against main on identical machine state show no measurable impact (warm single-edit 18.7 -> 19.8ms, cold ~equal, overlapping error bars). Packages bump to 4.0.0. Co-Authored-By: Claude Fable 5 --- .../DeserializeTimerAttribute.cs | 47 +++ .../DeserializeTimerFieldAttribute.cs | 18 +- .../FieldChangedAttribute.cs | 38 +++ .../ModernUO.Serialization.Annotations.csproj | 4 +- .../SaveFlagAttribute.cs | 45 +++ .../SerializableFieldChangedAttribute.cs | 20 +- .../SerializableFieldDefaultAttribute.cs | 24 +- .../SerializableFieldSaveFlagAttribute.cs | 18 +- .../TimerDriftAttribute.cs | 9 +- .../DiagnosticTests.cs | 12 +- .../LinkageTests.cs | 280 ++++++++++++++++++ .../ReadonlyFieldTests.cs | 4 +- .../SaveFlagTests.cs | 12 +- .../Snapshots/FieldModifiers/Input.cs | 2 +- ...Content.FieldLinkedItem.Serialization.g.cs | 123 ++++++++ .../Snapshots/FieldSideLinkage/Input.cs | 37 +++ ...estContent.ExternalItem.Serialization.g.cs | 9 +- .../Snapshots/NonServerNamespace/Input.cs | 2 +- .../Snapshots/SaveFlagsMultiEnum/Input.cs | 143 +++++---- .../Snapshots/SaveFlagsUlong/Input.cs | 71 +++-- .../Snapshots/SaveFlagsWithDefaults/Input.cs | 10 +- ...estContent.TrackedChild.Serialization.g.cs | 9 +- .../Snapshots/TimerAndDirtyTracking/Input.cs | 2 +- ....TestContent.TimersItem.Serialization.g.cs | 99 +++++++ .../Snapshots/Timers/Input.cs | 37 +++ .../Server.TestContent.TimersItem.v0.json | 22 ++ .../Diagnostics/DiagnosticDescriptors.cs | 40 ++- .../Diagnostics/DiagnosticInfo.cs | 6 +- .../ModernUO.Serialization.Generator.csproj | 4 +- ...SerializableEntityGeneration.BuildModel.cs | 216 +++++++++++--- .../Rules/TimerMigrationRule.cs | 41 ++- .../SymbolMetadata/SymbolMetadata.UO.cs | 61 ++-- ...ernUO.Serialization.SchemaGenerator.csproj | 4 +- README.md | 83 ++++-- 34 files changed, 1275 insertions(+), 277 deletions(-) create mode 100644 ModernUO.Serialization.Annotations/DeserializeTimerAttribute.cs create mode 100644 ModernUO.Serialization.Annotations/FieldChangedAttribute.cs create mode 100644 ModernUO.Serialization.Annotations/SaveFlagAttribute.cs create mode 100644 ModernUO.Serialization.Generator.Tests/LinkageTests.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Expected/Server.TestContent.TimersItem.Serialization.g.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Input.cs create mode 100644 ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Server.TestContent.TimersItem.v0.json diff --git a/ModernUO.Serialization.Annotations/DeserializeTimerAttribute.cs b/ModernUO.Serialization.Annotations/DeserializeTimerAttribute.cs new file mode 100644 index 0000000..7a7708a --- /dev/null +++ b/ModernUO.Serialization.Annotations/DeserializeTimerAttribute.cs @@ -0,0 +1,47 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2026 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: DeserializeTimerAttribute.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; + +namespace ModernUO.Serialization; + +/// +/// Declares how a serializable Timer field is restored at load. The named method must have the +/// signature void Method(TimeSpan delay) and is invoked only when a timer was running at +/// save, with its remaining delay. +/// +/// By default the timer drifts: its next tick is written as anchored time, so downtime does not +/// consume the remaining delay. Set to true for absolute +/// deadlines instead; the delay is then negative when the deadline passed during downtime. +/// +/// +/// [SerializableField(1)] +/// [DeserializeTimer(nameof(DeserializeRelockTimer))] +/// private Timer _relockTimer; +/// +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class DeserializeTimerAttribute : Attribute +{ + public string MethodName { get; } + + public bool WallClock { get; } + + public DeserializeTimerAttribute(string methodName, bool wallClock = false) + { + MethodName = methodName; + WallClock = wallClock; + } +} diff --git a/ModernUO.Serialization.Annotations/DeserializeTimerFieldAttribute.cs b/ModernUO.Serialization.Annotations/DeserializeTimerFieldAttribute.cs index 9751fb6..e544c20 100644 --- a/ModernUO.Serialization.Annotations/DeserializeTimerFieldAttribute.cs +++ b/ModernUO.Serialization.Annotations/DeserializeTimerFieldAttribute.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: DeserializeTimerFieldAttribute.cs * * * @@ -18,16 +18,16 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the specified serializable field, which must be a timer, -/// can be deserialized by this method. The method signature should look like this: -/// -/// [DeserializeTimerField(0)] -/// private void DeserializeTimer(TimeSpan delay) +/// Removed in v4. Timer serialization is declared with [DeserializeTimer(nameof(Method))] on +/// the timer field instead of an order-linked attribute on the method. The method is now +/// invoked only when a timer was running at save. /// [AttributeUsage(AttributeTargets.Method)] +[Obsolete("Removed in v4. Use [DeserializeTimer(nameof(Method))] on the timer field. The method is invoked only when a timer was running at save.", true)] public sealed class DeserializeTimerFieldAttribute : Attribute { - public int Order { get; } - - public DeserializeTimerFieldAttribute(int order) => Order = order; + [Obsolete("Removed in v4. Use [DeserializeTimer(nameof(Method))] on the timer field.", true)] + public DeserializeTimerFieldAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs new file mode 100644 index 0000000..9fc89b4 --- /dev/null +++ b/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs @@ -0,0 +1,38 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2026 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: FieldChangedAttribute.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; + +namespace ModernUO.Serialization; + +/// +/// Declares the change callback for a serializable field, on the field itself. The named +/// method must have the signature void Method(T oldValue, T newValue) where T is the +/// field's type; it is invoked by the generated setter after assignment. +/// +/// [SerializableField(2)] +/// [FieldChanged(nameof(OnLevelChanged))] +/// private int _level; +/// +/// Equivalent to placing [SerializableFieldChanged] on the method; declare one style per +/// field, not both. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class FieldChangedAttribute : Attribute +{ + public string MethodName { get; } + + public FieldChangedAttribute(string methodName) => MethodName = methodName; +} diff --git a/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj b/ModernUO.Serialization.Annotations/ModernUO.Serialization.Annotations.csproj index 2a8203b..092cd86 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 - 3.0.0 - 3.0.0 + 4.0.0 + 4.0.0 ModernUO.Serialization.Annotations ModernUO.Serialization true diff --git a/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs b/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs new file mode 100644 index 0000000..15fd186 --- /dev/null +++ b/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs @@ -0,0 +1,45 @@ +/************************************************************************* + * ModernUO * + * Copyright 2019-2026 - ModernUO Development Team * + * Email: hi@modernuo.com * + * File: SaveFlagAttribute.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; + +namespace ModernUO.Serialization; + +/// +/// Declares conditional serialization for a serializable field, on the field itself. The +/// first method (bool Method()) decides whether the value is written; the optional +/// second method (returning the field's type, no parameters) supplies the value at load when +/// it was not written. +/// +/// [SerializableField(0)] +/// [SaveFlag(nameof(ShouldSerializeName), nameof(NameDefaultValue))] +/// private string _name; +/// +/// Equivalent to placing [SerializableFieldSaveFlag] and [SerializableFieldDefault] on the +/// methods; declare one style per field, not both. +/// +[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +public sealed class SaveFlagAttribute : Attribute +{ + public string ShouldSerializeMethod { get; } + + public string DefaultValueMethod { get; } + + public SaveFlagAttribute(string shouldSerializeMethod, string defaultValueMethod = null) + { + ShouldSerializeMethod = shouldSerializeMethod; + DefaultValueMethod = defaultValueMethod; + } +} diff --git a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs index 60b8e7a..cd0d7e3 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SerializableFieldChangedAttribute.cs * * * @@ -18,13 +18,23 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the method should be called when the field with the same order value changes. -/// The method must have the signature: void MethodName(T oldValue, T newValue) where T is the field type. +/// Hints to the source generator that this method should be invoked by the generated setter +/// of the named serializable field after its value changes. The method signature must be +/// void Method(T oldValue, T newValue) where T is the field's type. +/// +/// [SerializableFieldChanged(nameof(_level))] +/// private void OnLevelChanged(int oldValue, int newValue) { } +/// /// [AttributeUsage(AttributeTargets.Method)] public sealed class SerializableFieldChangedAttribute : Attribute { - public int Order { get; } + public string FieldName { get; } - public SerializableFieldChangedAttribute(int order) => Order = order; + public SerializableFieldChangedAttribute(string fieldName) => FieldName = fieldName; + + [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldChanged(nameof(_field))], or [FieldChanged(...)] on the field.", true)] + public SerializableFieldChangedAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs index c686bce..9ef13e2 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SerializableFieldDefaultAttribute.cs * * * @@ -18,17 +18,25 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the field with the same order should use this default value -/// while deserializing. The default is used when the save flag indicates that we don't need to serialize the value -/// because this default can be used instead. +/// Hints to the source generator that the named serializable field should use this method's +/// return value while deserializing when the save flag indicates the value was not written. /// -/// Note: This is only used for the current version, not previous versions. Previous versions will always use null or default -/// for that type if it is not deserialized. +/// Note: This is only used for the current version, not previous versions. Previous versions +/// will always use null or default for that type if it is not deserialized. +/// +/// [SerializableFieldDefault(nameof(_charges))] +/// private int ChargesDefaultValue() => 8; +/// /// [AttributeUsage(AttributeTargets.Method)] public sealed class SerializableFieldDefaultAttribute : Attribute { - public int Order { get; } + public string FieldName { get; } - public SerializableFieldDefaultAttribute(int order) => Order = order; + public SerializableFieldDefaultAttribute(string fieldName) => FieldName = fieldName; + + [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldDefault(nameof(_field))], or [SaveFlag(...)] on the field.", true)] + public SerializableFieldDefaultAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs index c198ac8..045c093 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: SerializableFieldSaveFlagAttribute.cs * * * @@ -18,12 +18,22 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the field with the same order value should use a save flag. +/// Hints to the source generator that the named serializable field should use a save flag +/// determined by this method. +/// +/// [SerializableFieldSaveFlag(nameof(_name))] +/// private bool ShouldSerializeName() => _name != null; +/// /// [AttributeUsage(AttributeTargets.Method)] public sealed class SerializableFieldSaveFlagAttribute : Attribute { - public int Order { get; } + public string FieldName { get; } - public SerializableFieldSaveFlagAttribute(int order) => Order = order; + public SerializableFieldSaveFlagAttribute(string fieldName) => FieldName = fieldName; + + [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldSaveFlag(nameof(_field))], or [SaveFlag(...)] on the field.", true)] + public SerializableFieldSaveFlagAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/TimerDriftAttribute.cs b/ModernUO.Serialization.Annotations/TimerDriftAttribute.cs index 959d92e..8381c41 100644 --- a/ModernUO.Serialization.Annotations/TimerDriftAttribute.cs +++ b/ModernUO.Serialization.Annotations/TimerDriftAttribute.cs @@ -1,6 +1,6 @@ /************************************************************************* * ModernUO * - * Copyright 2019-2023 - ModernUO Development Team * + * Copyright 2019-2026 - ModernUO Development Team * * Email: hi@modernuo.com * * File: TimerDriftAttribute.cs * * * @@ -18,10 +18,13 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that this serializable timer field or property will drift -/// during deserialization. +/// Removed in v4. Timer serialization is declared with [DeserializeTimer(nameof(Method))] on +/// the timer field; drift is the default. Migrating a drifting timer changes its wire format +/// (anchored time), so bump the class's [SerializationGenerator] version and add a +/// MigrateFrom for the previous version. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] +[Obsolete("Removed in v4. Use [DeserializeTimer(nameof(Method))] on the timer field; drift is the default. Bump the class version - the wire format changes to anchored time.", true)] public sealed class TimerDriftAttribute : Attribute { } diff --git a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs index 8f54323..81b7609 100644 --- a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs +++ b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs @@ -246,10 +246,10 @@ public partial class DuplicateFlagItem : ISerializable [SerializableField(1)] private int _count; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName2() => _name != null; public Serial Serial => default; @@ -264,7 +264,7 @@ public void MarkDirty() { } } [Fact] - public void NegativeSaveFlagOrder_ReportsDiagnostic() + public void UnknownSaveFlagField_ReportsDiagnostic() { const string source = """ using ModernUO.Serialization; @@ -273,12 +273,12 @@ public void NegativeSaveFlagOrder_ReportsDiagnostic() namespace TestNamespace { [SerializationGenerator(0)] - public partial class NegativeFlagItem : ISerializable + public partial class UnknownFlagItem : ISerializable { [SerializableField(0)] private string _name; - [SerializableFieldSaveFlag(-1)] + [SerializableFieldSaveFlag("_missing")] private bool ShouldSerializeName() => _name != null; public Serial Serial => default; @@ -289,6 +289,6 @@ public void MarkDirty() { } var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - Assert.Contains(diagnostics, d => d.Id == "SG3006"); + Assert.Contains(diagnostics, d => d.Id == "SG3014"); } } diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs new file mode 100644 index 0000000..c6d16cc --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -0,0 +1,280 @@ +using ModernUO.Serialization.Generator.Tests.Helpers; +using Xunit; + +namespace ModernUO.Serialization.Generator.Tests; + +/// +/// Name-based linkage: field-side and method-side spellings must generate identical code, +/// and broken linkage must be reported instead of silently generating garbage. +/// +public class LinkageTests +{ + private const string MethodSideSource = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class LinkedItem : ISerializable + { + [SerializableField(0)] + private int _charges; + + [SerializableFieldSaveFlag(nameof(_charges))] + private bool ShouldSerializeCharges() => _charges != 8; + + [SerializableFieldDefault(nameof(_charges))] + private int ChargesDefaultValue() => 8; + + [SerializableField(1)] + private int _level; + + [SerializableFieldChanged(nameof(_level))] + private void OnLevelChanged(int oldValue, int newValue) + { + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + private const string FieldSideSource = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class LinkedItem : ISerializable + { + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] + private int _charges; + + private bool ShouldSerializeCharges() => _charges != 8; + + private int ChargesDefaultValue() => 8; + + [SerializableField(1)] + [FieldChanged(nameof(OnLevelChanged))] + private int _level; + + private void OnLevelChanged(int oldValue, int newValue) + { + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + [Fact] + public void FieldSideAndMethodSideLinkage_GenerateIdenticalCode() + { + var (methodDiags, methodSource) = SourceGeneratorTestHelper.RunGenerator(MethodSideSource); + var (fieldDiags, fieldSource) = SourceGeneratorTestHelper.RunGenerator(FieldSideSource); + + Assert.Empty(methodDiags.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); + Assert.Empty(fieldDiags.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); + Assert.Equal(methodSource, fieldSource); + } + + [Fact] + public void ConflictingLinkageStyles_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class ConflictedItem : ISerializable + { + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges))] + private int _charges; + + private bool ShouldSerializeCharges() => _charges != 8; + + [SerializableFieldSaveFlag(nameof(_charges))] + private bool AlsoShouldSerializeCharges() => _charges != 8; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3016"); + } + + [Fact] + public void FieldSideLinkage_MissingMethod_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class MissingMethodItem : ISerializable + { + [SerializableField(0)] + [SaveFlag("DoesNotExist")] + private int _charges; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3015"); + } + + [Fact] + public void MethodSideSaveFlag_WrongSignature_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class WrongShapeItem : ISerializable + { + [SerializableField(0)] + private int _charges; + + [SerializableFieldSaveFlag(nameof(_charges))] + private int ShouldSerializeCharges() => _charges; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3015"); + } + + [Fact] + public void DefaultWithoutSaveFlag_ReportsWarning() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class DanglingDefaultItem : ISerializable + { + [SerializableField(0)] + private int _charges; + + [SerializableFieldDefault(nameof(_charges))] + private int ChargesDefaultValue() => 8; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.NotNull(generatedSource); + Assert.Contains(diagnostics, d => d.Id == "SG3017"); + } + + [Fact] + public void DeserializeTimer_MissingMethod_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class BrokenTimerItem : ISerializable + { + [SerializableField(0)] + [DeserializeTimer("DoesNotExist")] + private Timer _timer; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3015"); + } + + [Fact] + public void SerializableTimer_WithoutDeserializeTimer_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class NakedTimerItem : ISerializable + { + [SerializableField(0)] + private Timer _timer; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3008"); + } +} diff --git a/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs b/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs index 4e69f61..78e0f8b 100644 --- a/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs +++ b/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs @@ -156,10 +156,10 @@ public partial class ReadonlyFlagItem : ISerializable [SerializableField(1)] private string _name; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_id))] private bool ShouldSerializeId() => _id != null; - [SerializableFieldSaveFlag(1)] + [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; public Serial Serial => default; diff --git a/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs b/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs index 92b4726..ddfdae2 100644 --- a/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs +++ b/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs @@ -22,7 +22,7 @@ public partial class SaveFlagItem : ISerializable [SerializableField(0)] private string _name; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; public Serial Serial => default; @@ -61,7 +61,7 @@ public partial class MultiFlagItem : ISerializable sb.AppendLine($" [SerializableField({i})]"); sb.AppendLine($" private string _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag({i})]"); + sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != null;"); sb.AppendLine(); } @@ -108,7 +108,7 @@ public partial class ManyFlagItem : ISerializable sb.AppendLine($" [SerializableField({i})]"); sb.AppendLine($" private int _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag({i})]"); + sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != 0;"); sb.AppendLine(); } @@ -149,7 +149,7 @@ public partial class HugeFlagItem : ISerializable sb.AppendLine($" [SerializableField({i})]"); sb.AppendLine($" private int _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag({i})]"); + sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != 0;"); sb.AppendLine(); } @@ -209,10 +209,10 @@ public partial class DefaultValueItem : ISerializable [SerializableField(0)] private int _count; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_count))] private bool ShouldSerializeCount() => _count != 0; - [SerializableFieldDefault(0)] + [SerializableFieldDefault(nameof(_count))] private int GetCountDefault() => 0; public Serial Serial => default; diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs index b2a4d0e..01379fb 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs @@ -20,7 +20,7 @@ public partial class FieldModifiersItem : ISerializable [InvalidateProperties] private int _level; - [SerializableFieldChanged(2)] + [SerializableFieldChanged(nameof(_level))] private void OnLevelChanged(int oldValue, int newValue) { } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs new file mode 100644 index 0000000..67380c1 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs @@ -0,0 +1,123 @@ +// +// 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 FieldLinkedItem + { + private const int SerializationVersion = 0; + + public string Name + { + get => _name; + set + { + if (value != _name) + { + _name = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public int Charges + { + get => _charges; + set + { + if (value != _charges) + { + _charges = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public int Level + { + get => _level; + set + { + var oldValue = _level; + if (value != _level) + { + _level = value; + Server.ISerializableExtensions.MarkDirty(this); + OnLevelChanged(oldValue, value); + } + } + } + + public FieldLinkedItem(Server.Serial serial) + { + Serial = serial; + } + + public virtual void Serialize(Server.IGenericWriter writer) + { + writer.WriteEncodedInt(SerializationVersion); + + var saveFlags = SaveFlag.None; + if (ShouldSerializeName()) + { + saveFlags |= SaveFlag.Name; + } + if (ShouldSerializeCharges()) + { + saveFlags |= SaveFlag.Charges; + } + writer.WriteEnum(saveFlags); + + if ((saveFlags & SaveFlag.Name) != 0) + { + writer.Write(_name); + } + + if ((saveFlags & SaveFlag.Charges) != 0) + { + writer.Write(_charges); + } + + writer.Write(_level); + } + + public virtual void Deserialize(Server.IGenericReader reader) + { + var version = reader.ReadEncodedInt(); + + var saveFlags = reader.ReadEnum(); + + if ((saveFlags & SaveFlag.Name) != 0) + { + _name = reader.ReadString(); + } + + if ((saveFlags & SaveFlag.Charges) != 0) + { + _charges = reader.ReadInt(); + } + else + { + _charges = ChargesDefaultValue(); + } + + _level = reader.ReadInt(); + } + + [System.Flags] + private enum SaveFlag + { + None = 0x00000000, + Name = 0x00000001, + Charges = 0x00000002, + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs new file mode 100644 index 0000000..9bf9bcc --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs @@ -0,0 +1,37 @@ +using System; +using ModernUO.Serialization; +using Server; + +namespace Server.TestContent +{ + [SerializationGenerator(0)] + public partial class FieldLinkedItem : ISerializable + { + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeName))] + private string _name; + + private bool ShouldSerializeName() => _name != null; + + [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] + private int _charges; + + private bool ShouldSerializeCharges() => _charges != 8; + + private int ChargesDefaultValue() => 8; + + [SerializableField(2)] + [FieldChanged(nameof(OnLevelChanged))] + private int _level; + + private void OnLevelChanged(int oldValue, int newValue) + { + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs index 0ea30de..1b15a39 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Expected/TestContent.ExternalItem.Serialization.g.cs @@ -61,7 +61,7 @@ public virtual void Serialize(Server.IGenericWriter writer) writer.Write(_name); - writer.Write(RefreshTimer?.Next ?? System.DateTime.MinValue); + writer.WriteAnchoredTime(RefreshTimer?.Next ?? System.DateTime.MinValue); } public virtual void Deserialize(Server.IGenericReader reader) @@ -79,9 +79,12 @@ public virtual void Deserialize(Server.IGenericReader reader) _name = reader.ReadString(); - var RefreshTimerNext = reader.ReadDateTime(); + var RefreshTimerNext = reader.ReadAnchoredTime(); var RefreshTimerDelay = RefreshTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : RefreshTimerNext - Server.Core.Now; - DeserializeRefreshTimer(RefreshTimerDelay); + if (RefreshTimerNext != System.DateTime.MinValue) + { + DeserializeRefreshTimer(RefreshTimerDelay); + } ValidateState(); Server.Timer.DelayCall(RebuildCaches); } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs index 7e2e1ec..d9b394e 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/NonServerNamespace/Input.cs @@ -11,9 +11,9 @@ public partial class ExternalItem : ISerializable private string _name; [SerializableField(1)] + [DeserializeTimer(nameof(DeserializeRefreshTimer))] private Timer _refreshTimer; - [DeserializeTimerField(1)] private void DeserializeRefreshTimer(TimeSpan delay) { _refreshTimer = new Timer(); diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs index c14d286..380627b 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs @@ -1,4 +1,3 @@ -using System; using ModernUO.Serialization; using Server; @@ -10,424 +9,424 @@ public partial class MultiEnumFlagsItem : ISerializable [SerializableField(0)] private string _field0; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_field0))] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] private string _field1; - [SerializableFieldSaveFlag(1)] + [SerializableFieldSaveFlag(nameof(_field1))] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] private string _field2; - [SerializableFieldSaveFlag(2)] + [SerializableFieldSaveFlag(nameof(_field2))] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] private string _field3; - [SerializableFieldSaveFlag(3)] + [SerializableFieldSaveFlag(nameof(_field3))] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] private string _field4; - [SerializableFieldSaveFlag(4)] + [SerializableFieldSaveFlag(nameof(_field4))] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] private string _field5; - [SerializableFieldSaveFlag(5)] + [SerializableFieldSaveFlag(nameof(_field5))] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] private string _field6; - [SerializableFieldSaveFlag(6)] + [SerializableFieldSaveFlag(nameof(_field6))] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] private string _field7; - [SerializableFieldSaveFlag(7)] + [SerializableFieldSaveFlag(nameof(_field7))] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] private string _field8; - [SerializableFieldSaveFlag(8)] + [SerializableFieldSaveFlag(nameof(_field8))] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] private string _field9; - [SerializableFieldSaveFlag(9)] + [SerializableFieldSaveFlag(nameof(_field9))] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] private string _field10; - [SerializableFieldSaveFlag(10)] + [SerializableFieldSaveFlag(nameof(_field10))] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] private string _field11; - [SerializableFieldSaveFlag(11)] + [SerializableFieldSaveFlag(nameof(_field11))] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] private string _field12; - [SerializableFieldSaveFlag(12)] + [SerializableFieldSaveFlag(nameof(_field12))] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] private string _field13; - [SerializableFieldSaveFlag(13)] + [SerializableFieldSaveFlag(nameof(_field13))] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] private string _field14; - [SerializableFieldSaveFlag(14)] + [SerializableFieldSaveFlag(nameof(_field14))] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] private string _field15; - [SerializableFieldSaveFlag(15)] + [SerializableFieldSaveFlag(nameof(_field15))] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] private string _field16; - [SerializableFieldSaveFlag(16)] + [SerializableFieldSaveFlag(nameof(_field16))] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] private string _field17; - [SerializableFieldSaveFlag(17)] + [SerializableFieldSaveFlag(nameof(_field17))] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] private string _field18; - [SerializableFieldSaveFlag(18)] + [SerializableFieldSaveFlag(nameof(_field18))] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] private string _field19; - [SerializableFieldSaveFlag(19)] + [SerializableFieldSaveFlag(nameof(_field19))] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] private string _field20; - [SerializableFieldSaveFlag(20)] + [SerializableFieldSaveFlag(nameof(_field20))] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] private string _field21; - [SerializableFieldSaveFlag(21)] + [SerializableFieldSaveFlag(nameof(_field21))] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] private string _field22; - [SerializableFieldSaveFlag(22)] + [SerializableFieldSaveFlag(nameof(_field22))] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] private string _field23; - [SerializableFieldSaveFlag(23)] + [SerializableFieldSaveFlag(nameof(_field23))] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] private string _field24; - [SerializableFieldSaveFlag(24)] + [SerializableFieldSaveFlag(nameof(_field24))] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] private string _field25; - [SerializableFieldSaveFlag(25)] + [SerializableFieldSaveFlag(nameof(_field25))] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] private string _field26; - [SerializableFieldSaveFlag(26)] + [SerializableFieldSaveFlag(nameof(_field26))] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] private string _field27; - [SerializableFieldSaveFlag(27)] + [SerializableFieldSaveFlag(nameof(_field27))] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] private string _field28; - [SerializableFieldSaveFlag(28)] + [SerializableFieldSaveFlag(nameof(_field28))] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] private string _field29; - [SerializableFieldSaveFlag(29)] + [SerializableFieldSaveFlag(nameof(_field29))] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] private string _field30; - [SerializableFieldSaveFlag(30)] + [SerializableFieldSaveFlag(nameof(_field30))] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] private string _field31; - [SerializableFieldSaveFlag(31)] + [SerializableFieldSaveFlag(nameof(_field31))] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] private string _field32; - [SerializableFieldSaveFlag(32)] + [SerializableFieldSaveFlag(nameof(_field32))] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] private string _field33; - [SerializableFieldSaveFlag(33)] + [SerializableFieldSaveFlag(nameof(_field33))] private bool ShouldSerializeField33() => _field33 != null; [SerializableField(34)] private string _field34; - [SerializableFieldSaveFlag(34)] + [SerializableFieldSaveFlag(nameof(_field34))] private bool ShouldSerializeField34() => _field34 != null; [SerializableField(35)] private string _field35; - [SerializableFieldSaveFlag(35)] + [SerializableFieldSaveFlag(nameof(_field35))] private bool ShouldSerializeField35() => _field35 != null; [SerializableField(36)] private string _field36; - [SerializableFieldSaveFlag(36)] + [SerializableFieldSaveFlag(nameof(_field36))] private bool ShouldSerializeField36() => _field36 != null; [SerializableField(37)] private string _field37; - [SerializableFieldSaveFlag(37)] + [SerializableFieldSaveFlag(nameof(_field37))] private bool ShouldSerializeField37() => _field37 != null; [SerializableField(38)] private string _field38; - [SerializableFieldSaveFlag(38)] + [SerializableFieldSaveFlag(nameof(_field38))] private bool ShouldSerializeField38() => _field38 != null; [SerializableField(39)] private string _field39; - [SerializableFieldSaveFlag(39)] + [SerializableFieldSaveFlag(nameof(_field39))] private bool ShouldSerializeField39() => _field39 != null; [SerializableField(40)] private string _field40; - [SerializableFieldSaveFlag(40)] + [SerializableFieldSaveFlag(nameof(_field40))] private bool ShouldSerializeField40() => _field40 != null; [SerializableField(41)] private string _field41; - [SerializableFieldSaveFlag(41)] + [SerializableFieldSaveFlag(nameof(_field41))] private bool ShouldSerializeField41() => _field41 != null; [SerializableField(42)] private string _field42; - [SerializableFieldSaveFlag(42)] + [SerializableFieldSaveFlag(nameof(_field42))] private bool ShouldSerializeField42() => _field42 != null; [SerializableField(43)] private string _field43; - [SerializableFieldSaveFlag(43)] + [SerializableFieldSaveFlag(nameof(_field43))] private bool ShouldSerializeField43() => _field43 != null; [SerializableField(44)] private string _field44; - [SerializableFieldSaveFlag(44)] + [SerializableFieldSaveFlag(nameof(_field44))] private bool ShouldSerializeField44() => _field44 != null; [SerializableField(45)] private string _field45; - [SerializableFieldSaveFlag(45)] + [SerializableFieldSaveFlag(nameof(_field45))] private bool ShouldSerializeField45() => _field45 != null; [SerializableField(46)] private string _field46; - [SerializableFieldSaveFlag(46)] + [SerializableFieldSaveFlag(nameof(_field46))] private bool ShouldSerializeField46() => _field46 != null; [SerializableField(47)] private string _field47; - [SerializableFieldSaveFlag(47)] + [SerializableFieldSaveFlag(nameof(_field47))] private bool ShouldSerializeField47() => _field47 != null; [SerializableField(48)] private string _field48; - [SerializableFieldSaveFlag(48)] + [SerializableFieldSaveFlag(nameof(_field48))] private bool ShouldSerializeField48() => _field48 != null; [SerializableField(49)] private string _field49; - [SerializableFieldSaveFlag(49)] + [SerializableFieldSaveFlag(nameof(_field49))] private bool ShouldSerializeField49() => _field49 != null; [SerializableField(50)] private string _field50; - [SerializableFieldSaveFlag(50)] + [SerializableFieldSaveFlag(nameof(_field50))] private bool ShouldSerializeField50() => _field50 != null; [SerializableField(51)] private string _field51; - [SerializableFieldSaveFlag(51)] + [SerializableFieldSaveFlag(nameof(_field51))] private bool ShouldSerializeField51() => _field51 != null; [SerializableField(52)] private string _field52; - [SerializableFieldSaveFlag(52)] + [SerializableFieldSaveFlag(nameof(_field52))] private bool ShouldSerializeField52() => _field52 != null; [SerializableField(53)] private string _field53; - [SerializableFieldSaveFlag(53)] + [SerializableFieldSaveFlag(nameof(_field53))] private bool ShouldSerializeField53() => _field53 != null; [SerializableField(54)] private string _field54; - [SerializableFieldSaveFlag(54)] + [SerializableFieldSaveFlag(nameof(_field54))] private bool ShouldSerializeField54() => _field54 != null; [SerializableField(55)] private string _field55; - [SerializableFieldSaveFlag(55)] + [SerializableFieldSaveFlag(nameof(_field55))] private bool ShouldSerializeField55() => _field55 != null; [SerializableField(56)] private string _field56; - [SerializableFieldSaveFlag(56)] + [SerializableFieldSaveFlag(nameof(_field56))] private bool ShouldSerializeField56() => _field56 != null; [SerializableField(57)] private string _field57; - [SerializableFieldSaveFlag(57)] + [SerializableFieldSaveFlag(nameof(_field57))] private bool ShouldSerializeField57() => _field57 != null; [SerializableField(58)] private string _field58; - [SerializableFieldSaveFlag(58)] + [SerializableFieldSaveFlag(nameof(_field58))] private bool ShouldSerializeField58() => _field58 != null; [SerializableField(59)] private string _field59; - [SerializableFieldSaveFlag(59)] + [SerializableFieldSaveFlag(nameof(_field59))] private bool ShouldSerializeField59() => _field59 != null; [SerializableField(60)] private string _field60; - [SerializableFieldSaveFlag(60)] + [SerializableFieldSaveFlag(nameof(_field60))] private bool ShouldSerializeField60() => _field60 != null; [SerializableField(61)] private string _field61; - [SerializableFieldSaveFlag(61)] + [SerializableFieldSaveFlag(nameof(_field61))] private bool ShouldSerializeField61() => _field61 != null; [SerializableField(62)] private string _field62; - [SerializableFieldSaveFlag(62)] + [SerializableFieldSaveFlag(nameof(_field62))] private bool ShouldSerializeField62() => _field62 != null; [SerializableField(63)] private string _field63; - [SerializableFieldSaveFlag(63)] + [SerializableFieldSaveFlag(nameof(_field63))] private bool ShouldSerializeField63() => _field63 != null; [SerializableField(64)] private string _field64; - [SerializableFieldSaveFlag(64)] + [SerializableFieldSaveFlag(nameof(_field64))] private bool ShouldSerializeField64() => _field64 != null; [SerializableField(65)] private string _field65; - [SerializableFieldSaveFlag(65)] + [SerializableFieldSaveFlag(nameof(_field65))] private bool ShouldSerializeField65() => _field65 != null; [SerializableField(66)] private string _field66; - [SerializableFieldSaveFlag(66)] + [SerializableFieldSaveFlag(nameof(_field66))] private bool ShouldSerializeField66() => _field66 != null; [SerializableField(67)] private string _field67; - [SerializableFieldSaveFlag(67)] + [SerializableFieldSaveFlag(nameof(_field67))] private bool ShouldSerializeField67() => _field67 != null; [SerializableField(68)] private string _field68; - [SerializableFieldSaveFlag(68)] + [SerializableFieldSaveFlag(nameof(_field68))] private bool ShouldSerializeField68() => _field68 != null; [SerializableField(69)] private string _field69; - [SerializableFieldSaveFlag(69)] + [SerializableFieldSaveFlag(nameof(_field69))] private bool ShouldSerializeField69() => _field69 != null; - public DateTime Created { get; set; } + public System.DateTime Created { get; set; } public Serial Serial { get; } public bool Deleted => false; public void Delete() { } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs index 8b4a646..8ba24c1 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs @@ -1,4 +1,3 @@ -using System; using ModernUO.Serialization; using Server; @@ -10,208 +9,208 @@ public partial class UlongFlagsItem : ISerializable [SerializableField(0)] private string _field0; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_field0))] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] private string _field1; - [SerializableFieldSaveFlag(1)] + [SerializableFieldSaveFlag(nameof(_field1))] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] private string _field2; - [SerializableFieldSaveFlag(2)] + [SerializableFieldSaveFlag(nameof(_field2))] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] private string _field3; - [SerializableFieldSaveFlag(3)] + [SerializableFieldSaveFlag(nameof(_field3))] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] private string _field4; - [SerializableFieldSaveFlag(4)] + [SerializableFieldSaveFlag(nameof(_field4))] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] private string _field5; - [SerializableFieldSaveFlag(5)] + [SerializableFieldSaveFlag(nameof(_field5))] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] private string _field6; - [SerializableFieldSaveFlag(6)] + [SerializableFieldSaveFlag(nameof(_field6))] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] private string _field7; - [SerializableFieldSaveFlag(7)] + [SerializableFieldSaveFlag(nameof(_field7))] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] private string _field8; - [SerializableFieldSaveFlag(8)] + [SerializableFieldSaveFlag(nameof(_field8))] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] private string _field9; - [SerializableFieldSaveFlag(9)] + [SerializableFieldSaveFlag(nameof(_field9))] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] private string _field10; - [SerializableFieldSaveFlag(10)] + [SerializableFieldSaveFlag(nameof(_field10))] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] private string _field11; - [SerializableFieldSaveFlag(11)] + [SerializableFieldSaveFlag(nameof(_field11))] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] private string _field12; - [SerializableFieldSaveFlag(12)] + [SerializableFieldSaveFlag(nameof(_field12))] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] private string _field13; - [SerializableFieldSaveFlag(13)] + [SerializableFieldSaveFlag(nameof(_field13))] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] private string _field14; - [SerializableFieldSaveFlag(14)] + [SerializableFieldSaveFlag(nameof(_field14))] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] private string _field15; - [SerializableFieldSaveFlag(15)] + [SerializableFieldSaveFlag(nameof(_field15))] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] private string _field16; - [SerializableFieldSaveFlag(16)] + [SerializableFieldSaveFlag(nameof(_field16))] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] private string _field17; - [SerializableFieldSaveFlag(17)] + [SerializableFieldSaveFlag(nameof(_field17))] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] private string _field18; - [SerializableFieldSaveFlag(18)] + [SerializableFieldSaveFlag(nameof(_field18))] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] private string _field19; - [SerializableFieldSaveFlag(19)] + [SerializableFieldSaveFlag(nameof(_field19))] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] private string _field20; - [SerializableFieldSaveFlag(20)] + [SerializableFieldSaveFlag(nameof(_field20))] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] private string _field21; - [SerializableFieldSaveFlag(21)] + [SerializableFieldSaveFlag(nameof(_field21))] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] private string _field22; - [SerializableFieldSaveFlag(22)] + [SerializableFieldSaveFlag(nameof(_field22))] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] private string _field23; - [SerializableFieldSaveFlag(23)] + [SerializableFieldSaveFlag(nameof(_field23))] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] private string _field24; - [SerializableFieldSaveFlag(24)] + [SerializableFieldSaveFlag(nameof(_field24))] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] private string _field25; - [SerializableFieldSaveFlag(25)] + [SerializableFieldSaveFlag(nameof(_field25))] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] private string _field26; - [SerializableFieldSaveFlag(26)] + [SerializableFieldSaveFlag(nameof(_field26))] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] private string _field27; - [SerializableFieldSaveFlag(27)] + [SerializableFieldSaveFlag(nameof(_field27))] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] private string _field28; - [SerializableFieldSaveFlag(28)] + [SerializableFieldSaveFlag(nameof(_field28))] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] private string _field29; - [SerializableFieldSaveFlag(29)] + [SerializableFieldSaveFlag(nameof(_field29))] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] private string _field30; - [SerializableFieldSaveFlag(30)] + [SerializableFieldSaveFlag(nameof(_field30))] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] private string _field31; - [SerializableFieldSaveFlag(31)] + [SerializableFieldSaveFlag(nameof(_field31))] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] private string _field32; - [SerializableFieldSaveFlag(32)] + [SerializableFieldSaveFlag(nameof(_field32))] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] private string _field33; - [SerializableFieldSaveFlag(33)] + [SerializableFieldSaveFlag(nameof(_field33))] private bool ShouldSerializeField33() => _field33 != null; - public DateTime Created { get; set; } + public System.DateTime Created { get; set; } public Serial Serial { get; } public bool Deleted => false; public void Delete() { } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs index fd5ec70..294b268 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs @@ -10,25 +10,25 @@ public partial class SaveFlagsItem : ISerializable [SerializableField(0)] private string _name; - [SerializableFieldSaveFlag(0)] + [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; [SerializableField(1)] private int _charges; - [SerializableFieldSaveFlag(1)] + [SerializableFieldSaveFlag(nameof(_charges))] private bool ShouldSerializeCharges() => _charges != 8; - [SerializableFieldDefault(1)] + [SerializableFieldDefault(nameof(_charges))] private int ChargesDefaultValue() => 8; [SerializableField(2)] private DateTime _expires; - [SerializableFieldSaveFlag(2)] + [SerializableFieldSaveFlag(nameof(_expires))] private bool ShouldSerializeExpires() => _expires != DateTime.MinValue; - [SerializableFieldDefault(2)] + [SerializableFieldDefault(nameof(_expires))] private DateTime ExpiresDefaultValue() => DateTime.MinValue; [SerializableField(3)] diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs index 7c128c5..599fec4 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Expected/Server.TestContent.TrackedChild.Serialization.g.cs @@ -50,7 +50,7 @@ public virtual void Serialize(Server.IGenericWriter writer) { writer.Write(SerializationVersion); - writer.Write(RefreshTimer?.Next ?? System.DateTime.MinValue); + writer.WriteAnchoredTime(RefreshTimer?.Next ?? System.DateTime.MinValue); writer.Write(_progress); } @@ -59,9 +59,12 @@ public virtual void Deserialize(Server.IGenericReader reader) { var version = reader.ReadInt(); - var RefreshTimerNext = reader.ReadDateTime(); + var RefreshTimerNext = reader.ReadAnchoredTime(); var RefreshTimerDelay = RefreshTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : RefreshTimerNext - Server.Core.Now; - DeserializeRefreshTimer(RefreshTimerDelay); + if (RefreshTimerNext != System.DateTime.MinValue) + { + DeserializeRefreshTimer(RefreshTimerDelay); + } _progress = reader.ReadInt(); } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Input.cs index 9898474..c8498d6 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/TimerAndDirtyTracking/Input.cs @@ -23,9 +23,9 @@ public partial class TrackedChild private OwnerEntity _owner; [SerializableField(0)] + [DeserializeTimer(nameof(DeserializeRefreshTimer))] private Timer _refreshTimer; - [DeserializeTimerField(0)] private void DeserializeRefreshTimer(TimeSpan delay) { _refreshTimer = new Timer(); diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Expected/Server.TestContent.TimersItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Expected/Server.TestContent.TimersItem.Serialization.g.cs new file mode 100644 index 0000000..2c7a296 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Expected/Server.TestContent.TimersItem.Serialization.g.cs @@ -0,0 +1,99 @@ +// +// 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 TimersItem + { + private const int SerializationVersion = 1; + + public Server.Timer DriftTimer + { + get => _driftTimer; + set + { + if (value != _driftTimer) + { + _driftTimer = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public Server.Timer DeadlineTimer + { + get => _deadlineTimer; + set + { + if (value != _deadlineTimer) + { + _deadlineTimer = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + + public TimersItem(Server.Serial serial) + { + Serial = serial; + } + + ref struct V0Content + { + internal readonly System.DateTime DriftTimerNext; + internal readonly System.TimeSpan DriftTimerDelay; + internal readonly System.DateTime DeadlineTimerNext; + internal readonly System.TimeSpan DeadlineTimerDelay; + internal V0Content(Server.IGenericReader reader, Server.TestContent.TimersItem entity) + { + DriftTimerNext = reader.ReadDeltaTime(); + DriftTimerDelay = DriftTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DriftTimerNext - Server.Core.Now; + DeadlineTimerNext = reader.ReadDateTime(); + DeadlineTimerDelay = DeadlineTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DeadlineTimerNext - Server.Core.Now; + } + } + + public virtual void Serialize(Server.IGenericWriter writer) + { + writer.WriteEncodedInt(SerializationVersion); + + writer.WriteAnchoredTime(DriftTimer?.Next ?? System.DateTime.MinValue); + + writer.Write(DeadlineTimer?.Next ?? System.DateTime.MinValue); + } + + 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 DriftTimerNext = reader.ReadAnchoredTime(); + var DriftTimerDelay = DriftTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DriftTimerNext - Server.Core.Now; + if (DriftTimerNext != System.DateTime.MinValue) + { + RestartDriftTimer(DriftTimerDelay); + } + + var DeadlineTimerNext = reader.ReadDateTime(); + var DeadlineTimerDelay = DeadlineTimerNext == System.DateTime.MinValue ? System.TimeSpan.MinValue : DeadlineTimerNext - Server.Core.Now; + if (DeadlineTimerNext != System.DateTime.MinValue) + { + RestartDeadlineTimer(DeadlineTimerDelay); + } + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Input.cs new file mode 100644 index 0000000..9f0bae6 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Input.cs @@ -0,0 +1,37 @@ +using System; +using ModernUO.Serialization; +using Server; + +namespace Server.TestContent +{ + [SerializationGenerator(1)] + public partial class TimersItem : ISerializable + { + [SerializableField(0)] + [DeserializeTimer(nameof(RestartDriftTimer))] + private Timer _driftTimer; + + private void RestartDriftTimer(TimeSpan delay) + { + _driftTimer = new Timer(); + } + + [SerializableField(1)] + [DeserializeTimer(nameof(RestartDeadlineTimer), wallClock: true)] + private Timer _deadlineTimer; + + private void RestartDeadlineTimer(TimeSpan delay) + { + _deadlineTimer = new Timer(); + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + + private void MigrateFrom(V0Content content) + { + } + } +} diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Server.TestContent.TimersItem.v0.json b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Server.TestContent.TimersItem.v0.json new file mode 100644 index 0000000..db8d822 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/Timers/Server.TestContent.TimersItem.v0.json @@ -0,0 +1,22 @@ +{ + "version": 0, + "type": "Server.TestContent.TimersItem", + "properties": [ + { + "name": "DriftTimer", + "type": "Server.Timer", + "rule": "TimerMigrationRule", + "ruleArguments": [ + "@TimerDrift" + ] + }, + { + "name": "DeadlineTimer", + "type": "Server.Timer", + "rule": "TimerMigrationRule", + "ruleArguments": [ + "" + ] + } + ] +} diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs index 70ff82b..ef76007 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -87,8 +87,8 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor SG3008 = new( "SG3008", - "Missing DeserializeTimerField attribute", - "Missing DeserializeTimerField attribute for '{0}'", + "Missing DeserializeTimer attribute", + "Serializable timer field '{0}' must declare [DeserializeTimer(nameof(Method))]", "ModernUO.Serialization.Generator", DiagnosticSeverity.Error, true @@ -139,6 +139,42 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor SG3014 = new( + "SG3014", + "Unknown serializable field reference", + "[{0}] references '{1}', which is not a serializable field or property of this type", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor SG3015 = new( + "SG3015", + "Linked method not found or invalid", + "Method '{0}' referenced by [{1}] was not found or does not match the expected signature '{2}'", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor SG3016 = new( + "SG3016", + "Conflicting linkage styles", + "Field '{0}' declares {1} on the field and on a method; declare one style, not both", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Error, + true + ); + + public static readonly DiagnosticDescriptor SG3017 = new( + "SG3017", + "Default value without a save flag", + "The default value for '{0}' is ignored because the field has no save flag", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Warning, + true + ); + public static DiagnosticDescriptor GeneratorCrashedDiagnostic(Exception e) => new( "SG0001", diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs index a4d1c65..187cec5 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs @@ -47,7 +47,11 @@ EquatableArray Args ["SG3010"] = DiagnosticDescriptors.SG3010, ["SG3011"] = DiagnosticDescriptors.SG3011, ["SG3012"] = DiagnosticDescriptors.SG3012, - ["SG3013"] = DiagnosticDescriptors.SG3013 + ["SG3013"] = DiagnosticDescriptors.SG3013, + ["SG3014"] = DiagnosticDescriptors.SG3014, + ["SG3015"] = DiagnosticDescriptors.SG3015, + ["SG3016"] = DiagnosticDescriptors.SG3016, + ["SG3017"] = DiagnosticDescriptors.SG3017 }; public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location location, params object[] args) diff --git a/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj b/ModernUO.Serialization.Generator/ModernUO.Serialization.Generator.csproj index b85a013..f248adf 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 - 3.0.0 - 3.0.0 + 4.0.0 + 4.0.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 252a071..855d215 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs @@ -130,49 +130,106 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a var version = (int)serializableAttr.ConstructorArguments[0].Value!; var encodedVersion = (bool)serializableAttr.ConstructorArguments[1].Value!; - // Save flag / default value methods, keyed by field order. + var buildWarnings = new List(); + + // Serializable member names -> (order, type), for name-based linkage. + var memberOrders = new Dictionary(); + + foreach (var (symbol, attributeData) in fields) + { + var order = (int)attributeData.ConstructorArguments[0].Value!; + if (order >= 0 && symbol is IFieldSymbol fieldSymbol) + { + memberOrders[fieldSymbol.Name] = (order, fieldSymbol.Type); + } + } + + foreach (var (symbol, attributeData) in properties) + { + var order = (int)attributeData.ConstructorArguments[0].Value!; + if (order < 0 || symbol is not IPropertySymbol propertySymbol) + { + continue; + } + + memberOrders[propertySymbol.Name] = (order, propertySymbol.Type); + + var useFieldName = (string)attributeData.ConstructorArguments[1].Value!; + if (!string.IsNullOrWhiteSpace(useFieldName)) + { + memberOrders[useFieldName] = (order, propertySymbol.Type); + } + } + + static bool IsSaveFlagShape(IMethodSymbol method) => + method is { ReturnsVoid: false, Parameters.Length: 0, ReturnType.SpecialType: SpecialType.System_Boolean }; + + static bool IsDefaultValueShape(IMethodSymbol method, ITypeSymbol fieldType) => + method is { ReturnsVoid: false, Parameters.Length: 0 } && + SymbolEqualityComparer.Default.Equals(method.ReturnType, fieldType); + + static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => + method is { ReturnsVoid: true, Parameters.Length: 2 } && + SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, fieldType) && + SymbolEqualityComparer.Default.Equals(method.Parameters[1].Type, fieldType); + + // Save flag / default-value linkage: method-side attributes name the field; the + // field-side [SaveFlag] names the methods. One style per field, not both. var serializableFieldSaveFlags = new SortedDictionary(); + foreach (var (symbol, attrData) in saveFlagMethods) { - var order = (int)attrData.ConstructorArguments[0].Value!; + var method = (IMethodSymbol)symbol; + var fieldName = attrData.ConstructorArguments[0].Value as string; - if (order < 0) + if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) + { + return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldSaveFlag", fieldName ?? ""); + } + + if (!IsSaveFlagShape(method)) { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, symbol.Name); + return Fail(DiagnosticDescriptors.SG3015, method.Name, "SerializableFieldSaveFlag", "bool Method()"); } - if (serializableFieldSaveFlags.ContainsKey(order)) + if (serializableFieldSaveFlags.ContainsKey(target.Order)) { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, order); + return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, target.Order); } - serializableFieldSaveFlags[order] = new SerializableFieldSaveFlagMethods + serializableFieldSaveFlags[target.Order] = new SerializableFieldSaveFlagMethods { - DetermineFieldShouldSerialize = (IMethodSymbol)symbol + DetermineFieldShouldSerialize = method }; } foreach (var (symbol, attrData) in defaultMethods) { - var order = (int)attrData.ConstructorArguments[0].Value!; + var method = (IMethodSymbol)symbol; + var fieldName = attrData.ConstructorArguments[0].Value as string; - if (order < 0) + if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) + { + return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldDefault", fieldName ?? ""); + } + + if (!IsDefaultValueShape(method, target.Type)) { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, symbol.Name); + return Fail(DiagnosticDescriptors.SG3015, method.Name, "SerializableFieldDefault", $"{target.Type} Method()"); } - // No save flag, so we ignore the default value - if (!serializableFieldSaveFlags.TryGetValue(order, out var methods)) + if (!serializableFieldSaveFlags.TryGetValue(target.Order, out var methods)) { + buildWarnings.Add(DiagnosticInfo.Create(DiagnosticDescriptors.SG3017, typeNode.GetLocation(), fieldName)); continue; } if (methods.GetFieldDefaultValue != null) { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, order); + return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, target.Order); } - serializableFieldSaveFlags[order] = methods with { GetFieldDefaultValue = (IMethodSymbol)symbol }; + serializableFieldSaveFlags[target.Order] = methods with { GetFieldDefaultValue = method }; } // Dirty tracking / MarkDirty resolution. @@ -220,23 +277,114 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a markDirtyMethod = null; } - // SerializableFieldChanged methods, keyed by order. + // Change callbacks: method-side attributes name the field; field-side [FieldChanged] + // names the method. One style per field, not both. var serializableFieldChangedMethods = new Dictionary(); foreach (var (symbol, attrData) in changedMethods) { - var order = (int)attrData.ConstructorArguments[0].Value!; + var method = (IMethodSymbol)symbol; + var fieldName = attrData.ConstructorArguments[0].Value as string; - if (order < 0) + if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) + { + return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldChanged", fieldName ?? ""); + } + + if (!IsChangedShape(method, target.Type)) + { + return Fail(DiagnosticDescriptors.SG3010, method.Name, target.Type.ToDisplayString()); + } + + if (serializableFieldChangedMethods.ContainsKey(target.Order)) { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, symbol.Name); + return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, target.Order); } - if (serializableFieldChangedMethods.ContainsKey(order)) + serializableFieldChangedMethods[target.Order] = method; + } + + // Field-side linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the + // serializable members themselves. + var timerLinks = new Dictionary(); + + foreach (var (symbol, attributeData) in fields.Concat(properties)) + { + token.ThrowIfCancellationRequested(); + + var order = (int)attributeData.ConstructorArguments[0].Value!; + if (order < 0 || !memberOrders.TryGetValue(symbol.Name, out var target)) { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, order); + continue; } - serializableFieldChangedMethods[order] = (IMethodSymbol)symbol; + foreach (var attr in symbol.GetAttributes()) + { + if (attr.IsSaveFlag(compilation)) + { + if (serializableFieldSaveFlags.ContainsKey(order)) + { + return Fail(DiagnosticDescriptors.SG3016, symbol.Name, "a save flag"); + } + + var shouldName = attr.ConstructorArguments[0].Value as string; + var shouldMethod = classSymbol.FindLinkedMethod(shouldName, IsSaveFlagShape); + if (shouldMethod == null) + { + return Fail(DiagnosticDescriptors.SG3015, shouldName ?? "", "SaveFlag", "bool Method()"); + } + + IMethodSymbol? defaultMethod = null; + var defaultName = attr.ConstructorArguments[1].Value as string; + if (defaultName != null) + { + defaultMethod = classSymbol.FindLinkedMethod(defaultName, m => IsDefaultValueShape(m, target.Type)); + if (defaultMethod == null) + { + return Fail(DiagnosticDescriptors.SG3015, defaultName, "SaveFlag", $"{target.Type} Method()"); + } + } + + serializableFieldSaveFlags[order] = new SerializableFieldSaveFlagMethods + { + DetermineFieldShouldSerialize = shouldMethod, + GetFieldDefaultValue = defaultMethod + }; + } + else if (attr.IsFieldChanged(compilation)) + { + if (serializableFieldChangedMethods.ContainsKey(order)) + { + return Fail(DiagnosticDescriptors.SG3016, symbol.Name, "a change callback"); + } + + var methodName = attr.ConstructorArguments[0].Value as string; + var method = classSymbol.FindLinkedMethod(methodName, m => IsChangedShape(m, target.Type)); + if (method == null) + { + return Fail( + DiagnosticDescriptors.SG3015, methodName ?? "", "FieldChanged", + $"void Method({target.Type} oldValue, {target.Type} newValue)" + ); + } + + serializableFieldChangedMethods[order] = method; + } + else if (attr.IsDeserializeTimer(compilation)) + { + var methodName = attr.ConstructorArguments[0].Value as string; + var method = classSymbol.FindLinkedMethod( + methodName, + m => m is { ReturnsVoid: true, Parameters.Length: 1 } && m.Parameters[0].Type.IsTimeSpan(compilation) + ); + + if (method == null) + { + return Fail(DiagnosticDescriptors.SG3015, methodName ?? "", "DeserializeTimer", "void Method(TimeSpan delay)"); + } + + timerLinks[order] = new TimerFieldModel(order, method.Name); + } + } } var serializableFieldSet = new SortedSet(new SerializablePropertyComparer()); @@ -360,22 +508,9 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a // Readonly fields cannot have setters - force to null var effectiveSetterAccessor = fieldSymbol.IsReadOnly ? (Accessibility?)null : setterAccessor; + // Signatures are validated during linkage resolution. serializableFieldChangedMethods.TryGetValue(order, out var fieldChangedMethod); - if (fieldChangedMethod != null) - { - var fieldType = fieldSymbol.Type; - var isValidSignature = fieldChangedMethod.ReturnsVoid && - fieldChangedMethod.Parameters.Length == 2 && - SymbolEqualityComparer.Default.Equals(fieldChangedMethod.Parameters[0].Type, fieldType) && - SymbolEqualityComparer.Default.Equals(fieldChangedMethod.Parameters[1].Type, fieldType); - - if (!isValidSignature) - { - return Fail(DiagnosticDescriptors.SG3010, fieldChangedMethod.Name, fieldType.ToDisplayString()); - } - } - var invalidateProperties = allAttributes.Any( attr => attr.AttributeClass?.Equals( compilation.GetCachedTypeByMetadataName(SymbolMetadata.INVALIDATE_PROPERTIES_ATTRIBUTE), @@ -471,7 +606,7 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a } } - // Timer fields need a [DeserializeTimerField] method to rebuild the timer on load. + // Timer fields need a [DeserializeTimer] declaration to rebuild the timer on load. var timerFields = new List(); foreach (var field in serializableFields) { @@ -480,13 +615,12 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a continue; } - var timerMethod = classSymbol.GetDeserializeTimerMethod(compilation, field.Order); - if (timerMethod == null) + if (!timerLinks.TryGetValue(field.Order, out var timerField)) { return Fail(DiagnosticDescriptors.SG3008, field.Name); } - timerFields.Add(new TimerFieldModel(field.Order, timerMethod.Name)); + timerFields.Add(timerField); } // AfterDeserialization callbacks, in member order. @@ -567,6 +701,6 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a location ); - return new SerializationModelResult(model, EquatableArray.Empty); + return new SerializationModelResult(model, buildWarnings.ToEquatableArray()); } } diff --git a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs index 69dc1f8..63d7e28 100644 --- a/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs +++ b/ModernUO.Serialization.Generator/SerializableMigration/Rules/TimerMigrationRule.cs @@ -39,9 +39,20 @@ out string[] ruleArguments return false; } - ruleArguments = attributes.Any(a => a.IsTimerDrift(compilation)) - ? ["@TimerDrift"] - : [""]; + // Drift is the default: [DeserializeTimer(..., wallClock: true)] opts into absolute + // deadlines. "@TimerDrift" is the legacy delta-time marker and is never written to new + // schemas; it survives only in old-version migration files. + var wallClock = false; + foreach (var attr in attributes) + { + if (attr.IsDeserializeTimer(compilation)) + { + wallClock = (bool)attr.ConstructorArguments[1].Value!; + break; + } + } + + ruleArguments = wallClock ? [""] : ["@AnchoredTimer"]; return true; } @@ -70,10 +81,15 @@ public override void GenerateDeserializationMethod( } var propertyName = property.Name; - var ruleArguments = property.RuleArguments; - var driftTimer = ruleArguments![0].Contains("@TimerDrift"); + var readTimer = property.RuleArguments![0] switch + { + "@AnchoredTimer" => "reader.ReadAnchoredTime()", + // Historical format: pre-v4 saves stored drifting timers as delta time. Old-version + // migration schemas pin this marker, so the read path must exist permanently. + "@TimerDrift" => "reader.ReadDeltaTime()", + _ => "reader.ReadDateTime()" + }; - var readTimer = driftTimer ? "reader.ReadDeltaTime()" : "reader.ReadDateTime()"; var useVar = isMigration ? "" : "var "; source.AppendLine($"{indent}{useVar}{propertyName}Next = {readTimer};"); source.AppendLine($"{indent}{useVar}{propertyName}Delay = {propertyName}Next == System.DateTime.MinValue ? System.TimeSpan.MinValue : {propertyName}Next - Server.Core.Now;"); @@ -89,10 +105,7 @@ public override void GenerateSerializationMethod(StringBuilder source, string in } var propertyName = property.Name; - var ruleArguments = property.RuleArguments; - var driftTimer = ruleArguments![0].Contains("@TimerDrift"); - - var writerMethod = driftTimer ? "WriteDeltaTime" : "Write"; + var writerMethod = property.RuleArguments![0] == "@AnchoredTimer" ? "WriteAnchoredTime" : "Write"; source.AppendLine($"{indent}writer.{writerMethod}({propertyName}?.Next ?? System.DateTime.MinValue);"); } @@ -100,12 +113,16 @@ public void PostDeserializeMethod( StringBuilder source, string indent, SerializableProperty property, SerializationModel model ) { - // Resolved during model building; SG3008 fires there when the method is missing. + // Resolved during model building; SG3008 fires there when the declaration is missing. + // The method is invoked only when a timer was running at save. foreach (var timerField in model.TimerFields) { if (timerField.Order == property.Order) { - source.AppendLine($"{indent}{timerField.DeserializeMethodName}({property.Name}Delay);"); + source.AppendLine($"{indent}if ({property.Name}Next != System.DateTime.MinValue)"); + source.AppendLine($"{indent}{{"); + source.AppendLine($"{indent} {timerField.DeserializeMethodName}({property.Name}Delay);"); + source.AppendLine($"{indent}}}"); return; } } diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs index a764f12..6a23d9c 100644 --- a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs +++ b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs @@ -13,6 +13,7 @@ * along with this program. If not, see . * *************************************************************************/ +using System; using System.Collections.Immutable; using System.Linq; using Microsoft.CodeAnalysis; @@ -33,8 +34,9 @@ public static partial class SymbolMetadata public const string ENCODED_INT_ATTRIBUTE = "ModernUO.Serialization.EncodedIntAttribute"; public const string CAN_BE_NULL_ATTRIBUTE = "ModernUO.Serialization.CanBeNullAttribute"; public const string TIDY_ATTRIBUTE = "ModernUO.Serialization.TidyAttribute"; - public const string TIMER_DRIFT_ATTRIBUTE = "ModernUO.Serialization.TimerDriftAttribute"; - public const string DESERIALIZE_TIMER_FIELD_ATTRIBUTE = "ModernUO.Serialization.DeserializeTimerFieldAttribute"; + public const string DESERIALIZE_TIMER_ATTRIBUTE = "ModernUO.Serialization.DeserializeTimerAttribute"; + public const string SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SaveFlagAttribute"; + public const string FIELD_CHANGED_ATTRIBUTE = "ModernUO.Serialization.FieldChangedAttribute"; public const string SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldSaveFlagAttribute"; public const string SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldDefaultAttribute"; public const string SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldChangedAttribute"; @@ -90,8 +92,14 @@ public bool IsSerializedPropertyAttr(Compilation compilation, out ITypeSymbol ge public bool IsCanBeNull(Compilation compilation) => attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(CAN_BE_NULL_ATTRIBUTE)) == true; - public bool IsTimerDrift(Compilation compilation) => - attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(TIMER_DRIFT_ATTRIBUTE)) == true; + public bool IsDeserializeTimer(Compilation compilation) => + attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(DESERIALIZE_TIMER_ATTRIBUTE)) == true; + + public bool IsSaveFlag(Compilation compilation) => + attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(SAVE_FLAG_ATTRIBUTE)) == true; + + public bool IsFieldChanged(Compilation compilation) => + attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(FIELD_CHANGED_ATTRIBUTE)) == true; } public static bool IsTimer(this ITypeSymbol symbol, Compilation compilation) => @@ -317,38 +325,25 @@ public bool HasInequalityOperator() } /// - /// Finds the [DeserializeTimerField] method for the given field order, or null. + /// Finds an instance method by name with the expected shape, or null. The shape check + /// is supplied by the caller so each linkage attribute validates its own contract. /// - public IMethodSymbol GetDeserializeTimerMethod(Compilation compilation, int order) + public IMethodSymbol FindLinkedMethod(string methodName, Func hasValidSignature) { - return symbol - .GetMembers() - .OfType() - .FirstOrDefault( - m => - { - if (!m.ReturnsVoid || m.Parameters.Length != 1 || !m.Parameters[0].Type.IsTimeSpan(compilation)) - { - return false; - } + if (methodName == null) + { + return null; + } - return m.GetAttributes() - .FirstOrDefault( - attr => - { - if (!SymbolEqualityComparer.Default.Equals( - attr.AttributeClass, - compilation.GetCachedTypeByMetadataName(DESERIALIZE_TIMER_FIELD_ATTRIBUTE) - )) - { - return false; - } - - return (int)attr.ConstructorArguments[0].Value! == order; - } - ) != null; - } - ); + foreach (var member in symbol.GetMembers(methodName)) + { + if (member is IMethodSymbol { IsStatic: false } method && hasValidSignature(method)) + { + return method; + } + } + + return null; } public bool HasPublicSerializeMethod(Compilation compilation) diff --git a/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj b/ModernUO.Serialization.SchemaGenerator/ModernUO.Serialization.SchemaGenerator.csproj index 655290d..0fa885a 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 - 3.0.0 - 3.0.0 + 4.0.0 + 4.0.0 true ModernUOSchemaGenerator true diff --git a/README.md b/README.md index 2ada1e0..87e48ba 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,11 @@ The ModernUO serialization source generator takes the boilerplate out of writing While it is not the most elegant solution (recommendations and contributions are welcome!), it should handle most use-cases. ### How to install -Add `ModernUO.SerializationGenerator` as an analyzer project reference: +Add `ModernUO.Serialization.Generator` and `ModernUO.Serialization.Annotations` as package references: ```xml - - TargetFramework=netstandard2.0 - Analyzer - false - all - + + ``` @@ -248,20 +244,15 @@ Here is a complete example of how we would convert this: [SerializationGenerator(3, false)] public partial class DeathRobe : Robe { - [TimerDrift] + // The timer's next tick is written as anchored time by default, so downtime does not + // consume the remaining delay. Use wallClock: true for absolute deadlines instead. [SerializableField(0)] + [DeserializeTimer(nameof(DeserializeDecayTimer))] private Timer _decayTimer; - // Since the field is a timer, we need to tell the source generator how to convert from a time span to an actual timer. + // Invoked only when a timer was running at save, with its remaining delay. // This is a void instead of returning a Timer for flexiblity. - [DeserializeTimerField(0)] - private void DeserializeDecayTimer(TimeSpan delay) - { - if (delay != TimeSpan.MinValue) - { - BeginDecay(delay); - } - } + private void DeserializeDecayTimer(TimeSpan delay) => BeginDecay(delay); [Constructible] public DeathRobe() @@ -309,3 +300,61 @@ Here is a complete example of how we would convert this: } } ``` + +## v4 Linkage and Timers + +v4 removes order-based linkage between fields and their companion methods. Attributes that +reference a serializable field take `nameof(_field)` instead of the field's order, and every +linkage can alternatively be declared on the field itself: + +```cs + // Method-side: attributes on the methods name the field. + [SerializableField(0)] + private int _charges; + + [SerializableFieldSaveFlag(nameof(_charges))] + private bool ShouldSerializeCharges() => _charges != 8; + + [SerializableFieldDefault(nameof(_charges))] + private int ChargesDefaultValue() => 8; + + // Field-side: one attribute on the field names the methods. Same generated code. + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] + private int _charges; + + // Change callbacks work the same way, in either style: + [SerializableField(1)] + [FieldChanged(nameof(OnLevelChanged))] + private int _level; +``` + +Timers are declared on the field with `[DeserializeTimer]`, replacing `[TimerDrift]` and +`[DeserializeTimerField]`: + +```cs + [SerializableField(0)] + [DeserializeTimer(nameof(RestartDecayTimer))] + private Timer _decayTimer; + + private void RestartDecayTimer(TimeSpan delay) => BeginDecay(delay); +``` + +By default the timer drifts: its next tick is stored as anchored time, so downtime does not +consume the remaining delay, and the restart method is invoked only when a timer was actually +running at save. Use `[DeserializeTimer(nameof(Method), wallClock: true)]` for absolute +deadlines; the delay is then negative when the deadline passed during downtime. + +### Migrating from v3 + +- Replace `[SerializableFieldSaveFlag(order)]`, `[SerializableFieldDefault(order)]`, and + `[SerializableFieldChanged(order)]` with `nameof(_field)` (or move them onto the field as + `[SaveFlag]` / `[FieldChanged]`). These conversions do not change the wire format. +- Replace `[TimerDrift]` + `[DeserializeTimerField(order)]` with + `[DeserializeTimer(nameof(Method))]` on the timer field. Drifting timers change wire format + (delta time to anchored time), so bump the class's `[SerializationGenerator]` version and + add a `MigrateFrom` for the previous version; old saves keep reading correctly through the + migration schema. Wall-clock timers (no `[TimerDrift]` before) keep their format: use + `wallClock: true` and no version bump is needed. +- Remove `delay != TimeSpan.MinValue` checks from restart methods; they are no longer called + when no timer was running. From 0e3f1a916b1a8a88a0a68238ecfcbee7338d7dd6 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:02:42 -0700 Subject: [PATCH 2/5] Remove method-side linkage attributes; field-side is the only style SerializableFieldSaveFlag, SerializableFieldDefault, and SerializableFieldChanged become error-[Obsolete] shells. With linkage declared only on the field, the mistakes those diagnostics guarded become unrepresentable: a default cannot exist without a save flag, a linkage cannot reference a missing field, duplicates are compiler-enforced, and styles cannot conflict. SG3010/SG3014/SG3016/SG3017 are retired; SG3015 remains the single method-resolution error. Snapshot Expected files are unchanged, proving the conversion is emission-identical. Co-Authored-By: Claude Fable 5 --- .../FieldChangedAttribute.cs | 2 - .../SaveFlagAttribute.cs | 4 +- .../SerializableFieldChangedAttribute.cs | 17 +- .../SerializableFieldDefaultAttribute.cs | 20 +-- .../SerializableFieldSaveFlagAttribute.cs | 17 +- .../DiagnosticTests.cs | 63 ------- .../LinkageTests.cs | 153 +++++++--------- .../ReadonlyFieldTests.cs | 4 +- .../SaveFlagTests.cs | 11 +- .../Snapshots/FieldModifiers/Input.cs | 2 +- .../Snapshots/SaveFlagsMultiEnum/Input.cs | 140 +++++++-------- .../Snapshots/SaveFlagsUlong/Input.cs | 68 ++++---- .../Snapshots/SaveFlagsWithDefaults/Input.cs | 8 +- .../Diagnostics/DiagnosticDescriptors.cs | 39 +---- .../Diagnostics/DiagnosticInfo.cs | 6 +- ...SerializableEntityGeneration.BuildModel.cs | 165 ++---------------- .../SymbolMetadata/SymbolMetadata.UO.cs | 23 --- README.md | 35 ++-- 18 files changed, 233 insertions(+), 544 deletions(-) diff --git a/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs index 9fc89b4..5db2482 100644 --- a/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs +++ b/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs @@ -26,8 +26,6 @@ namespace ModernUO.Serialization; /// [FieldChanged(nameof(OnLevelChanged))] /// private int _level; /// -/// Equivalent to placing [SerializableFieldChanged] on the method; declare one style per -/// field, not both. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class FieldChangedAttribute : Attribute diff --git a/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs b/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs index 15fd186..cb54e59 100644 --- a/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs +++ b/ModernUO.Serialization.Annotations/SaveFlagAttribute.cs @@ -27,8 +27,8 @@ namespace ModernUO.Serialization; /// [SaveFlag(nameof(ShouldSerializeName), nameof(NameDefaultValue))] /// private string _name; /// -/// Equivalent to placing [SerializableFieldSaveFlag] and [SerializableFieldDefault] on the -/// methods; declare one style per field, not both. +/// When the second method is omitted, the field keeps its default value at load when the +/// save flag indicates the value was not written. /// [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] public sealed class SaveFlagAttribute : Attribute diff --git a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs index cd0d7e3..3b855a9 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs @@ -18,22 +18,17 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that this method should be invoked by the generated setter -/// of the named serializable field after its value changes. The method signature must be -/// void Method(T oldValue, T newValue) where T is the field's type. -/// -/// [SerializableFieldChanged(nameof(_level))] -/// private void OnLevelChanged(int oldValue, int newValue) { } -/// +/// Removed in v4. Change callbacks are declared on the serializable field itself: +/// [FieldChanged(nameof(Method))]. This conversion does not change the wire format. /// [AttributeUsage(AttributeTargets.Method)] +[Obsolete("Removed in v4. Declare [FieldChanged(nameof(Method))] on the serializable field instead. The wire format does not change.", true)] public sealed class SerializableFieldChangedAttribute : Attribute { - public string FieldName { get; } - - public SerializableFieldChangedAttribute(string fieldName) => FieldName = fieldName; + public SerializableFieldChangedAttribute(string fieldName) + { + } - [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldChanged(nameof(_field))], or [FieldChanged(...)] on the field.", true)] public SerializableFieldChangedAttribute(int order) { } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs index 9ef13e2..5562937 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs @@ -18,24 +18,18 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the named serializable field should use this method's -/// return value while deserializing when the save flag indicates the value was not written. -/// -/// Note: This is only used for the current version, not previous versions. Previous versions -/// will always use null or default for that type if it is not deserialized. -/// -/// [SerializableFieldDefault(nameof(_charges))] -/// private int ChargesDefaultValue() => 8; -/// +/// Removed in v4. A default value is declared as the second argument of the field's save +/// flag: [SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))], so it +/// cannot exist without one. This conversion does not change the wire format. /// [AttributeUsage(AttributeTargets.Method)] +[Obsolete("Removed in v4. Declare [SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))] on the serializable field instead. The wire format does not change.", true)] public sealed class SerializableFieldDefaultAttribute : Attribute { - public string FieldName { get; } - - public SerializableFieldDefaultAttribute(string fieldName) => FieldName = fieldName; + public SerializableFieldDefaultAttribute(string fieldName) + { + } - [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldDefault(nameof(_field))], or [SaveFlag(...)] on the field.", true)] public SerializableFieldDefaultAttribute(int order) { } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs index 045c093..12a8fd8 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs @@ -18,21 +18,18 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the named serializable field should use a save flag -/// determined by this method. -/// -/// [SerializableFieldSaveFlag(nameof(_name))] -/// private bool ShouldSerializeName() => _name != null; -/// +/// Removed in v4. Save flags are declared on the serializable field itself: +/// [SaveFlag(nameof(ShouldSerializeMethod))]. This conversion does not change the +/// wire format. /// [AttributeUsage(AttributeTargets.Method)] +[Obsolete("Removed in v4. Declare [SaveFlag(nameof(ShouldSerializeMethod))] on the serializable field instead. The wire format does not change.", true)] public sealed class SerializableFieldSaveFlagAttribute : Attribute { - public string FieldName { get; } - - public SerializableFieldSaveFlagAttribute(string fieldName) => FieldName = fieldName; + public SerializableFieldSaveFlagAttribute(string fieldName) + { + } - [Obsolete("Order-based linkage was removed in v4. Use [SerializableFieldSaveFlag(nameof(_field))], or [SaveFlag(...)] on the field.", true)] public SerializableFieldSaveFlagAttribute(int order) { } diff --git a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs index 81b7609..8a564d5 100644 --- a/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs +++ b/ModernUO.Serialization.Generator.Tests/DiagnosticTests.cs @@ -228,67 +228,4 @@ public void MarkDirty() { } Assert.NotNull(generatedSource); } - [Fact] - public void DuplicateSaveFlagOrder_ReportsDiagnostic() - { - const string source = """ - using ModernUO.Serialization; - using Server; - - namespace TestNamespace - { - [SerializationGenerator(0)] - public partial class DuplicateFlagItem : ISerializable - { - [SerializableField(0)] - private string _name; - - [SerializableField(1)] - private int _count; - - [SerializableFieldSaveFlag(nameof(_name))] - private bool ShouldSerializeName() => _name != null; - - [SerializableFieldSaveFlag(nameof(_name))] - private bool ShouldSerializeName2() => _name != null; - - public Serial Serial => default; - public void MarkDirty() { } - } - } - """; - - var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - - Assert.Contains(diagnostics, d => d.Id == "SG3003"); - } - - [Fact] - public void UnknownSaveFlagField_ReportsDiagnostic() - { - const string source = """ - using ModernUO.Serialization; - using Server; - - namespace TestNamespace - { - [SerializationGenerator(0)] - public partial class UnknownFlagItem : ISerializable - { - [SerializableField(0)] - private string _name; - - [SerializableFieldSaveFlag("_missing")] - private bool ShouldSerializeName() => _name != null; - - public Serial Serial => default; - public void MarkDirty() { } - } - } - """; - - var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - - Assert.Contains(diagnostics, d => d.Id == "SG3014"); - } } diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs index c6d16cc..3c4a75f 100644 --- a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -4,93 +4,60 @@ namespace ModernUO.Serialization.Generator.Tests; /// -/// Name-based linkage: field-side and method-side spellings must generate identical code, -/// and broken linkage must be reported instead of silently generating garbage. +/// Name-based linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the field name +/// their companion methods, and broken linkage must be reported instead of silently +/// generating garbage. /// public class LinkageTests { - private const string MethodSideSource = """ - using System; - using ModernUO.Serialization; - using Server; - - namespace Server.TestContent - { - [SerializationGenerator(0)] - public partial class LinkedItem : ISerializable - { - [SerializableField(0)] - private int _charges; - - [SerializableFieldSaveFlag(nameof(_charges))] - private bool ShouldSerializeCharges() => _charges != 8; - - [SerializableFieldDefault(nameof(_charges))] - private int ChargesDefaultValue() => 8; - - [SerializableField(1)] - private int _level; + [Fact] + public void FieldSideLinkage_GeneratesLinkedCode() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; - [SerializableFieldChanged(nameof(_level))] - private void OnLevelChanged(int oldValue, int newValue) + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class LinkedItem : ISerializable { - } + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] + private int _charges; - public DateTime Created { get; set; } - public Serial Serial { get; } - public bool Deleted => false; - public void Delete() { } - } - } - """; - - private const string FieldSideSource = """ - using System; - using ModernUO.Serialization; - using Server; - - namespace Server.TestContent - { - [SerializationGenerator(0)] - public partial class LinkedItem : ISerializable - { - [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] - private int _charges; + private bool ShouldSerializeCharges() => _charges != 8; - private bool ShouldSerializeCharges() => _charges != 8; + private int ChargesDefaultValue() => 8; - private int ChargesDefaultValue() => 8; + [SerializableField(1)] + [FieldChanged(nameof(OnLevelChanged))] + private int _level; - [SerializableField(1)] - [FieldChanged(nameof(OnLevelChanged))] - private int _level; + private void OnLevelChanged(int oldValue, int newValue) + { + } - private void OnLevelChanged(int oldValue, int newValue) - { + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } } - - public DateTime Created { get; set; } - public Serial Serial { get; } - public bool Deleted => false; - public void Delete() { } } - } - """; + """; - [Fact] - public void FieldSideAndMethodSideLinkage_GenerateIdenticalCode() - { - var (methodDiags, methodSource) = SourceGeneratorTestHelper.RunGenerator(MethodSideSource); - var (fieldDiags, fieldSource) = SourceGeneratorTestHelper.RunGenerator(FieldSideSource); + var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); - Assert.Empty(methodDiags.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); - Assert.Empty(fieldDiags.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); - Assert.Equal(methodSource, fieldSource); + Assert.Empty(diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); + Assert.NotNull(generatedSource); + Assert.Contains("ShouldSerializeCharges()", generatedSource); + Assert.Contains("ChargesDefaultValue()", generatedSource); + Assert.Contains("OnLevelChanged(", generatedSource); } [Fact] - public void ConflictingLinkageStyles_ReportsDiagnostic() + public void SaveFlag_MissingMethod_ReportsDiagnostic() { const string source = """ using System; @@ -100,17 +67,12 @@ public void ConflictingLinkageStyles_ReportsDiagnostic() namespace Server.TestContent { [SerializationGenerator(0)] - public partial class ConflictedItem : ISerializable + public partial class MissingMethodItem : ISerializable { [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeCharges))] + [SaveFlag("DoesNotExist")] private int _charges; - private bool ShouldSerializeCharges() => _charges != 8; - - [SerializableFieldSaveFlag(nameof(_charges))] - private bool AlsoShouldSerializeCharges() => _charges != 8; - public DateTime Created { get; set; } public Serial Serial { get; } public bool Deleted => false; @@ -121,11 +83,11 @@ public void Delete() { } var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - Assert.Contains(diagnostics, d => d.Id == "SG3016"); + Assert.Contains(diagnostics, d => d.Id == "SG3015"); } [Fact] - public void FieldSideLinkage_MissingMethod_ReportsDiagnostic() + public void SaveFlag_WrongSignature_ReportsDiagnostic() { const string source = """ using System; @@ -135,12 +97,14 @@ public void FieldSideLinkage_MissingMethod_ReportsDiagnostic() namespace Server.TestContent { [SerializationGenerator(0)] - public partial class MissingMethodItem : ISerializable + public partial class WrongShapeItem : ISerializable { [SerializableField(0)] - [SaveFlag("DoesNotExist")] + [SaveFlag(nameof(ShouldSerializeCharges))] private int _charges; + private int ShouldSerializeCharges() => _charges; + public DateTime Created { get; set; } public Serial Serial { get; } public bool Deleted => false; @@ -155,7 +119,7 @@ public void Delete() { } } [Fact] - public void MethodSideSaveFlag_WrongSignature_ReportsDiagnostic() + public void SaveFlag_DefaultWrongSignature_ReportsDiagnostic() { const string source = """ using System; @@ -165,13 +129,15 @@ public void MethodSideSaveFlag_WrongSignature_ReportsDiagnostic() namespace Server.TestContent { [SerializationGenerator(0)] - public partial class WrongShapeItem : ISerializable + public partial class WrongDefaultItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] private int _charges; - [SerializableFieldSaveFlag(nameof(_charges))] - private int ShouldSerializeCharges() => _charges; + private bool ShouldSerializeCharges() => _charges != 8; + + private string ChargesDefaultValue() => "8"; public DateTime Created { get; set; } public Serial Serial { get; } @@ -187,7 +153,7 @@ public void Delete() { } } [Fact] - public void DefaultWithoutSaveFlag_ReportsWarning() + public void FieldChanged_WrongSignature_ReportsDiagnostic() { const string source = """ using System; @@ -197,13 +163,15 @@ public void DefaultWithoutSaveFlag_ReportsWarning() namespace Server.TestContent { [SerializationGenerator(0)] - public partial class DanglingDefaultItem : ISerializable + public partial class WrongChangedItem : ISerializable { [SerializableField(0)] - private int _charges; + [FieldChanged(nameof(OnLevelChanged))] + private int _level; - [SerializableFieldDefault(nameof(_charges))] - private int ChargesDefaultValue() => 8; + private void OnLevelChanged(int newValue) + { + } public DateTime Created { get; set; } public Serial Serial { get; } @@ -213,10 +181,9 @@ public void Delete() { } } """; - var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - Assert.NotNull(generatedSource); - Assert.Contains(diagnostics, d => d.Id == "SG3017"); + Assert.Contains(diagnostics, d => d.Id == "SG3015"); } [Fact] diff --git a/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs b/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs index 78e0f8b..5f8327c 100644 --- a/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs +++ b/ModernUO.Serialization.Generator.Tests/ReadonlyFieldTests.cs @@ -151,15 +151,15 @@ namespace TestNamespace public partial class ReadonlyFlagItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeId))] private readonly string _id; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeName))] private string _name; - [SerializableFieldSaveFlag(nameof(_id))] private bool ShouldSerializeId() => _id != null; - [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; public Serial Serial => default; diff --git a/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs b/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs index ddfdae2..770a928 100644 --- a/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs +++ b/ModernUO.Serialization.Generator.Tests/SaveFlagTests.cs @@ -20,9 +20,9 @@ namespace TestNamespace public partial class SaveFlagItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeName))] private string _name; - [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; public Serial Serial => default; @@ -59,9 +59,9 @@ public partial class MultiFlagItem : ISerializable for (int i = 0; i < 5; i++) { sb.AppendLine($" [SerializableField({i})]"); + sb.AppendLine($" [SaveFlag(nameof(ShouldSerializeField{i}))]"); sb.AppendLine($" private string _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != null;"); sb.AppendLine(); } @@ -106,9 +106,9 @@ public partial class ManyFlagItem : ISerializable for (int i = 0; i < 35; i++) { sb.AppendLine($" [SerializableField({i})]"); + sb.AppendLine($" [SaveFlag(nameof(ShouldSerializeField{i}))]"); sb.AppendLine($" private int _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != 0;"); sb.AppendLine(); } @@ -147,9 +147,9 @@ public partial class HugeFlagItem : ISerializable for (int i = 0; i < 70; i++) { sb.AppendLine($" [SerializableField({i})]"); + sb.AppendLine($" [SaveFlag(nameof(ShouldSerializeField{i}))]"); sb.AppendLine($" private int _field{i};"); sb.AppendLine(); - sb.AppendLine($" [SerializableFieldSaveFlag(nameof(_field{i}))]"); sb.AppendLine($" private bool ShouldSerializeField{i}() => _field{i} != 0;"); sb.AppendLine(); } @@ -207,12 +207,11 @@ namespace TestNamespace public partial class DefaultValueItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCount), nameof(GetCountDefault))] private int _count; - [SerializableFieldSaveFlag(nameof(_count))] private bool ShouldSerializeCount() => _count != 0; - [SerializableFieldDefault(nameof(_count))] private int GetCountDefault() => 0; public Serial Serial => default; diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs index 01379fb..914a0fc 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs @@ -18,9 +18,9 @@ public partial class FieldModifiersItem : ISerializable [SerializableField(2)] [InvalidateProperties] + [FieldChanged(nameof(OnLevelChanged))] private int _level; - [SerializableFieldChanged(nameof(_level))] private void OnLevelChanged(int oldValue, int newValue) { } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs index 380627b..b27147b 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsMultiEnum/Input.cs @@ -7,423 +7,423 @@ namespace Server.TestContent public partial class MultiEnumFlagsItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeField0))] private string _field0; - [SerializableFieldSaveFlag(nameof(_field0))] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeField1))] private string _field1; - [SerializableFieldSaveFlag(nameof(_field1))] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeField2))] private string _field2; - [SerializableFieldSaveFlag(nameof(_field2))] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] + [SaveFlag(nameof(ShouldSerializeField3))] private string _field3; - [SerializableFieldSaveFlag(nameof(_field3))] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] + [SaveFlag(nameof(ShouldSerializeField4))] private string _field4; - [SerializableFieldSaveFlag(nameof(_field4))] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] + [SaveFlag(nameof(ShouldSerializeField5))] private string _field5; - [SerializableFieldSaveFlag(nameof(_field5))] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] + [SaveFlag(nameof(ShouldSerializeField6))] private string _field6; - [SerializableFieldSaveFlag(nameof(_field6))] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] + [SaveFlag(nameof(ShouldSerializeField7))] private string _field7; - [SerializableFieldSaveFlag(nameof(_field7))] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] + [SaveFlag(nameof(ShouldSerializeField8))] private string _field8; - [SerializableFieldSaveFlag(nameof(_field8))] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] + [SaveFlag(nameof(ShouldSerializeField9))] private string _field9; - [SerializableFieldSaveFlag(nameof(_field9))] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] + [SaveFlag(nameof(ShouldSerializeField10))] private string _field10; - [SerializableFieldSaveFlag(nameof(_field10))] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] + [SaveFlag(nameof(ShouldSerializeField11))] private string _field11; - [SerializableFieldSaveFlag(nameof(_field11))] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] + [SaveFlag(nameof(ShouldSerializeField12))] private string _field12; - [SerializableFieldSaveFlag(nameof(_field12))] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] + [SaveFlag(nameof(ShouldSerializeField13))] private string _field13; - [SerializableFieldSaveFlag(nameof(_field13))] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] + [SaveFlag(nameof(ShouldSerializeField14))] private string _field14; - [SerializableFieldSaveFlag(nameof(_field14))] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] + [SaveFlag(nameof(ShouldSerializeField15))] private string _field15; - [SerializableFieldSaveFlag(nameof(_field15))] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] + [SaveFlag(nameof(ShouldSerializeField16))] private string _field16; - [SerializableFieldSaveFlag(nameof(_field16))] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] + [SaveFlag(nameof(ShouldSerializeField17))] private string _field17; - [SerializableFieldSaveFlag(nameof(_field17))] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] + [SaveFlag(nameof(ShouldSerializeField18))] private string _field18; - [SerializableFieldSaveFlag(nameof(_field18))] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] + [SaveFlag(nameof(ShouldSerializeField19))] private string _field19; - [SerializableFieldSaveFlag(nameof(_field19))] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] + [SaveFlag(nameof(ShouldSerializeField20))] private string _field20; - [SerializableFieldSaveFlag(nameof(_field20))] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] + [SaveFlag(nameof(ShouldSerializeField21))] private string _field21; - [SerializableFieldSaveFlag(nameof(_field21))] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] + [SaveFlag(nameof(ShouldSerializeField22))] private string _field22; - [SerializableFieldSaveFlag(nameof(_field22))] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] + [SaveFlag(nameof(ShouldSerializeField23))] private string _field23; - [SerializableFieldSaveFlag(nameof(_field23))] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] + [SaveFlag(nameof(ShouldSerializeField24))] private string _field24; - [SerializableFieldSaveFlag(nameof(_field24))] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] + [SaveFlag(nameof(ShouldSerializeField25))] private string _field25; - [SerializableFieldSaveFlag(nameof(_field25))] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] + [SaveFlag(nameof(ShouldSerializeField26))] private string _field26; - [SerializableFieldSaveFlag(nameof(_field26))] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] + [SaveFlag(nameof(ShouldSerializeField27))] private string _field27; - [SerializableFieldSaveFlag(nameof(_field27))] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] + [SaveFlag(nameof(ShouldSerializeField28))] private string _field28; - [SerializableFieldSaveFlag(nameof(_field28))] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] + [SaveFlag(nameof(ShouldSerializeField29))] private string _field29; - [SerializableFieldSaveFlag(nameof(_field29))] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] + [SaveFlag(nameof(ShouldSerializeField30))] private string _field30; - [SerializableFieldSaveFlag(nameof(_field30))] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] + [SaveFlag(nameof(ShouldSerializeField31))] private string _field31; - [SerializableFieldSaveFlag(nameof(_field31))] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] + [SaveFlag(nameof(ShouldSerializeField32))] private string _field32; - [SerializableFieldSaveFlag(nameof(_field32))] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] + [SaveFlag(nameof(ShouldSerializeField33))] private string _field33; - [SerializableFieldSaveFlag(nameof(_field33))] private bool ShouldSerializeField33() => _field33 != null; [SerializableField(34)] + [SaveFlag(nameof(ShouldSerializeField34))] private string _field34; - [SerializableFieldSaveFlag(nameof(_field34))] private bool ShouldSerializeField34() => _field34 != null; [SerializableField(35)] + [SaveFlag(nameof(ShouldSerializeField35))] private string _field35; - [SerializableFieldSaveFlag(nameof(_field35))] private bool ShouldSerializeField35() => _field35 != null; [SerializableField(36)] + [SaveFlag(nameof(ShouldSerializeField36))] private string _field36; - [SerializableFieldSaveFlag(nameof(_field36))] private bool ShouldSerializeField36() => _field36 != null; [SerializableField(37)] + [SaveFlag(nameof(ShouldSerializeField37))] private string _field37; - [SerializableFieldSaveFlag(nameof(_field37))] private bool ShouldSerializeField37() => _field37 != null; [SerializableField(38)] + [SaveFlag(nameof(ShouldSerializeField38))] private string _field38; - [SerializableFieldSaveFlag(nameof(_field38))] private bool ShouldSerializeField38() => _field38 != null; [SerializableField(39)] + [SaveFlag(nameof(ShouldSerializeField39))] private string _field39; - [SerializableFieldSaveFlag(nameof(_field39))] private bool ShouldSerializeField39() => _field39 != null; [SerializableField(40)] + [SaveFlag(nameof(ShouldSerializeField40))] private string _field40; - [SerializableFieldSaveFlag(nameof(_field40))] private bool ShouldSerializeField40() => _field40 != null; [SerializableField(41)] + [SaveFlag(nameof(ShouldSerializeField41))] private string _field41; - [SerializableFieldSaveFlag(nameof(_field41))] private bool ShouldSerializeField41() => _field41 != null; [SerializableField(42)] + [SaveFlag(nameof(ShouldSerializeField42))] private string _field42; - [SerializableFieldSaveFlag(nameof(_field42))] private bool ShouldSerializeField42() => _field42 != null; [SerializableField(43)] + [SaveFlag(nameof(ShouldSerializeField43))] private string _field43; - [SerializableFieldSaveFlag(nameof(_field43))] private bool ShouldSerializeField43() => _field43 != null; [SerializableField(44)] + [SaveFlag(nameof(ShouldSerializeField44))] private string _field44; - [SerializableFieldSaveFlag(nameof(_field44))] private bool ShouldSerializeField44() => _field44 != null; [SerializableField(45)] + [SaveFlag(nameof(ShouldSerializeField45))] private string _field45; - [SerializableFieldSaveFlag(nameof(_field45))] private bool ShouldSerializeField45() => _field45 != null; [SerializableField(46)] + [SaveFlag(nameof(ShouldSerializeField46))] private string _field46; - [SerializableFieldSaveFlag(nameof(_field46))] private bool ShouldSerializeField46() => _field46 != null; [SerializableField(47)] + [SaveFlag(nameof(ShouldSerializeField47))] private string _field47; - [SerializableFieldSaveFlag(nameof(_field47))] private bool ShouldSerializeField47() => _field47 != null; [SerializableField(48)] + [SaveFlag(nameof(ShouldSerializeField48))] private string _field48; - [SerializableFieldSaveFlag(nameof(_field48))] private bool ShouldSerializeField48() => _field48 != null; [SerializableField(49)] + [SaveFlag(nameof(ShouldSerializeField49))] private string _field49; - [SerializableFieldSaveFlag(nameof(_field49))] private bool ShouldSerializeField49() => _field49 != null; [SerializableField(50)] + [SaveFlag(nameof(ShouldSerializeField50))] private string _field50; - [SerializableFieldSaveFlag(nameof(_field50))] private bool ShouldSerializeField50() => _field50 != null; [SerializableField(51)] + [SaveFlag(nameof(ShouldSerializeField51))] private string _field51; - [SerializableFieldSaveFlag(nameof(_field51))] private bool ShouldSerializeField51() => _field51 != null; [SerializableField(52)] + [SaveFlag(nameof(ShouldSerializeField52))] private string _field52; - [SerializableFieldSaveFlag(nameof(_field52))] private bool ShouldSerializeField52() => _field52 != null; [SerializableField(53)] + [SaveFlag(nameof(ShouldSerializeField53))] private string _field53; - [SerializableFieldSaveFlag(nameof(_field53))] private bool ShouldSerializeField53() => _field53 != null; [SerializableField(54)] + [SaveFlag(nameof(ShouldSerializeField54))] private string _field54; - [SerializableFieldSaveFlag(nameof(_field54))] private bool ShouldSerializeField54() => _field54 != null; [SerializableField(55)] + [SaveFlag(nameof(ShouldSerializeField55))] private string _field55; - [SerializableFieldSaveFlag(nameof(_field55))] private bool ShouldSerializeField55() => _field55 != null; [SerializableField(56)] + [SaveFlag(nameof(ShouldSerializeField56))] private string _field56; - [SerializableFieldSaveFlag(nameof(_field56))] private bool ShouldSerializeField56() => _field56 != null; [SerializableField(57)] + [SaveFlag(nameof(ShouldSerializeField57))] private string _field57; - [SerializableFieldSaveFlag(nameof(_field57))] private bool ShouldSerializeField57() => _field57 != null; [SerializableField(58)] + [SaveFlag(nameof(ShouldSerializeField58))] private string _field58; - [SerializableFieldSaveFlag(nameof(_field58))] private bool ShouldSerializeField58() => _field58 != null; [SerializableField(59)] + [SaveFlag(nameof(ShouldSerializeField59))] private string _field59; - [SerializableFieldSaveFlag(nameof(_field59))] private bool ShouldSerializeField59() => _field59 != null; [SerializableField(60)] + [SaveFlag(nameof(ShouldSerializeField60))] private string _field60; - [SerializableFieldSaveFlag(nameof(_field60))] private bool ShouldSerializeField60() => _field60 != null; [SerializableField(61)] + [SaveFlag(nameof(ShouldSerializeField61))] private string _field61; - [SerializableFieldSaveFlag(nameof(_field61))] private bool ShouldSerializeField61() => _field61 != null; [SerializableField(62)] + [SaveFlag(nameof(ShouldSerializeField62))] private string _field62; - [SerializableFieldSaveFlag(nameof(_field62))] private bool ShouldSerializeField62() => _field62 != null; [SerializableField(63)] + [SaveFlag(nameof(ShouldSerializeField63))] private string _field63; - [SerializableFieldSaveFlag(nameof(_field63))] private bool ShouldSerializeField63() => _field63 != null; [SerializableField(64)] + [SaveFlag(nameof(ShouldSerializeField64))] private string _field64; - [SerializableFieldSaveFlag(nameof(_field64))] private bool ShouldSerializeField64() => _field64 != null; [SerializableField(65)] + [SaveFlag(nameof(ShouldSerializeField65))] private string _field65; - [SerializableFieldSaveFlag(nameof(_field65))] private bool ShouldSerializeField65() => _field65 != null; [SerializableField(66)] + [SaveFlag(nameof(ShouldSerializeField66))] private string _field66; - [SerializableFieldSaveFlag(nameof(_field66))] private bool ShouldSerializeField66() => _field66 != null; [SerializableField(67)] + [SaveFlag(nameof(ShouldSerializeField67))] private string _field67; - [SerializableFieldSaveFlag(nameof(_field67))] private bool ShouldSerializeField67() => _field67 != null; [SerializableField(68)] + [SaveFlag(nameof(ShouldSerializeField68))] private string _field68; - [SerializableFieldSaveFlag(nameof(_field68))] private bool ShouldSerializeField68() => _field68 != null; [SerializableField(69)] + [SaveFlag(nameof(ShouldSerializeField69))] private string _field69; - [SerializableFieldSaveFlag(nameof(_field69))] private bool ShouldSerializeField69() => _field69 != null; public System.DateTime Created { get; set; } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs index 8ba24c1..31560ec 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsUlong/Input.cs @@ -7,207 +7,207 @@ namespace Server.TestContent public partial class UlongFlagsItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeField0))] private string _field0; - [SerializableFieldSaveFlag(nameof(_field0))] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeField1))] private string _field1; - [SerializableFieldSaveFlag(nameof(_field1))] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeField2))] private string _field2; - [SerializableFieldSaveFlag(nameof(_field2))] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] + [SaveFlag(nameof(ShouldSerializeField3))] private string _field3; - [SerializableFieldSaveFlag(nameof(_field3))] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] + [SaveFlag(nameof(ShouldSerializeField4))] private string _field4; - [SerializableFieldSaveFlag(nameof(_field4))] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] + [SaveFlag(nameof(ShouldSerializeField5))] private string _field5; - [SerializableFieldSaveFlag(nameof(_field5))] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] + [SaveFlag(nameof(ShouldSerializeField6))] private string _field6; - [SerializableFieldSaveFlag(nameof(_field6))] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] + [SaveFlag(nameof(ShouldSerializeField7))] private string _field7; - [SerializableFieldSaveFlag(nameof(_field7))] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] + [SaveFlag(nameof(ShouldSerializeField8))] private string _field8; - [SerializableFieldSaveFlag(nameof(_field8))] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] + [SaveFlag(nameof(ShouldSerializeField9))] private string _field9; - [SerializableFieldSaveFlag(nameof(_field9))] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] + [SaveFlag(nameof(ShouldSerializeField10))] private string _field10; - [SerializableFieldSaveFlag(nameof(_field10))] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] + [SaveFlag(nameof(ShouldSerializeField11))] private string _field11; - [SerializableFieldSaveFlag(nameof(_field11))] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] + [SaveFlag(nameof(ShouldSerializeField12))] private string _field12; - [SerializableFieldSaveFlag(nameof(_field12))] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] + [SaveFlag(nameof(ShouldSerializeField13))] private string _field13; - [SerializableFieldSaveFlag(nameof(_field13))] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] + [SaveFlag(nameof(ShouldSerializeField14))] private string _field14; - [SerializableFieldSaveFlag(nameof(_field14))] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] + [SaveFlag(nameof(ShouldSerializeField15))] private string _field15; - [SerializableFieldSaveFlag(nameof(_field15))] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] + [SaveFlag(nameof(ShouldSerializeField16))] private string _field16; - [SerializableFieldSaveFlag(nameof(_field16))] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] + [SaveFlag(nameof(ShouldSerializeField17))] private string _field17; - [SerializableFieldSaveFlag(nameof(_field17))] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] + [SaveFlag(nameof(ShouldSerializeField18))] private string _field18; - [SerializableFieldSaveFlag(nameof(_field18))] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] + [SaveFlag(nameof(ShouldSerializeField19))] private string _field19; - [SerializableFieldSaveFlag(nameof(_field19))] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] + [SaveFlag(nameof(ShouldSerializeField20))] private string _field20; - [SerializableFieldSaveFlag(nameof(_field20))] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] + [SaveFlag(nameof(ShouldSerializeField21))] private string _field21; - [SerializableFieldSaveFlag(nameof(_field21))] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] + [SaveFlag(nameof(ShouldSerializeField22))] private string _field22; - [SerializableFieldSaveFlag(nameof(_field22))] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] + [SaveFlag(nameof(ShouldSerializeField23))] private string _field23; - [SerializableFieldSaveFlag(nameof(_field23))] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] + [SaveFlag(nameof(ShouldSerializeField24))] private string _field24; - [SerializableFieldSaveFlag(nameof(_field24))] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] + [SaveFlag(nameof(ShouldSerializeField25))] private string _field25; - [SerializableFieldSaveFlag(nameof(_field25))] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] + [SaveFlag(nameof(ShouldSerializeField26))] private string _field26; - [SerializableFieldSaveFlag(nameof(_field26))] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] + [SaveFlag(nameof(ShouldSerializeField27))] private string _field27; - [SerializableFieldSaveFlag(nameof(_field27))] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] + [SaveFlag(nameof(ShouldSerializeField28))] private string _field28; - [SerializableFieldSaveFlag(nameof(_field28))] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] + [SaveFlag(nameof(ShouldSerializeField29))] private string _field29; - [SerializableFieldSaveFlag(nameof(_field29))] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] + [SaveFlag(nameof(ShouldSerializeField30))] private string _field30; - [SerializableFieldSaveFlag(nameof(_field30))] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] + [SaveFlag(nameof(ShouldSerializeField31))] private string _field31; - [SerializableFieldSaveFlag(nameof(_field31))] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] + [SaveFlag(nameof(ShouldSerializeField32))] private string _field32; - [SerializableFieldSaveFlag(nameof(_field32))] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] + [SaveFlag(nameof(ShouldSerializeField33))] private string _field33; - [SerializableFieldSaveFlag(nameof(_field33))] private bool ShouldSerializeField33() => _field33 != null; public System.DateTime Created { get; set; } diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs index 294b268..c66af27 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/SaveFlagsWithDefaults/Input.cs @@ -8,27 +8,25 @@ namespace Server.TestContent public partial class SaveFlagsItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeName))] private string _name; - [SerializableFieldSaveFlag(nameof(_name))] private bool ShouldSerializeName() => _name != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] private int _charges; - [SerializableFieldSaveFlag(nameof(_charges))] private bool ShouldSerializeCharges() => _charges != 8; - [SerializableFieldDefault(nameof(_charges))] private int ChargesDefaultValue() => 8; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeExpires), nameof(ExpiresDefaultValue))] private DateTime _expires; - [SerializableFieldSaveFlag(nameof(_expires))] private bool ShouldSerializeExpires() => _expires != DateTime.MinValue; - [SerializableFieldDefault(nameof(_expires))] private DateTime ExpiresDefaultValue() => DateTime.MinValue; [SerializableField(3)] diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs index ef76007..86b9916 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -103,14 +103,10 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor SG3010 = new( - "SG3010", - "SerializableFieldChanged method has invalid signature", - "Method '{0}' must have signature 'void {0}({1} oldValue, {1} newValue)' for field of type '{1}'", - "ModernUO.Serialization.Generator", - DiagnosticSeverity.Error, - true - ); + // SG3010 (changed-method signature), SG3014 (unknown field reference), SG3016 + // (conflicting linkage styles), and SG3017 (default without save flag) were retired in + // v4: field-side linkage makes those mistakes unrepresentable, and SG3015 covers every + // remaining method-resolution failure. Do not reuse the numbers. public static readonly DiagnosticDescriptor SG3011 = new( "SG3011", @@ -139,15 +135,6 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor SG3014 = new( - "SG3014", - "Unknown serializable field reference", - "[{0}] references '{1}', which is not a serializable field or property of this type", - "ModernUO.Serialization.Generator", - DiagnosticSeverity.Error, - true - ); - public static readonly DiagnosticDescriptor SG3015 = new( "SG3015", "Linked method not found or invalid", @@ -157,24 +144,6 @@ public static class DiagnosticDescriptors true ); - public static readonly DiagnosticDescriptor SG3016 = new( - "SG3016", - "Conflicting linkage styles", - "Field '{0}' declares {1} on the field and on a method; declare one style, not both", - "ModernUO.Serialization.Generator", - DiagnosticSeverity.Error, - true - ); - - public static readonly DiagnosticDescriptor SG3017 = new( - "SG3017", - "Default value without a save flag", - "The default value for '{0}' is ignored because the field has no save flag", - "ModernUO.Serialization.Generator", - DiagnosticSeverity.Warning, - true - ); - public static DiagnosticDescriptor GeneratorCrashedDiagnostic(Exception e) => new( "SG0001", diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs index 187cec5..20862ec 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs @@ -44,14 +44,10 @@ EquatableArray Args ["SG3007"] = DiagnosticDescriptors.SG3007, ["SG3008"] = DiagnosticDescriptors.SG3008, ["SG3009"] = DiagnosticDescriptors.SG3009, - ["SG3010"] = DiagnosticDescriptors.SG3010, ["SG3011"] = DiagnosticDescriptors.SG3011, ["SG3012"] = DiagnosticDescriptors.SG3012, ["SG3013"] = DiagnosticDescriptors.SG3013, - ["SG3014"] = DiagnosticDescriptors.SG3014, - ["SG3015"] = DiagnosticDescriptors.SG3015, - ["SG3016"] = DiagnosticDescriptors.SG3016, - ["SG3017"] = DiagnosticDescriptors.SG3017 + ["SG3015"] = DiagnosticDescriptors.SG3015 }; public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location location, params object[] args) diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs index 855d215..bf5e7a5 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs @@ -61,9 +61,6 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a // Gather annotated members from the attributed declaration. var fields = new List<(ISymbol, AttributeData)>(); var properties = new List<(ISymbol, AttributeData)>(); - var saveFlagMethods = new List<(ISymbol, AttributeData)>(); - var defaultMethods = new List<(ISymbol, AttributeData)>(); - var changedMethods = new List<(ISymbol, AttributeData)>(); ISymbol? dirtyTrackingEntity = null; foreach (var m in typeNode.Members) @@ -103,24 +100,6 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a } } } - else if (m is MethodDeclarationSyntax methodNode) - { - if (ctx.SemanticModel.GetDeclaredSymbol(methodNode) is IMethodSymbol methodSymbol) - { - if (methodSymbol.TryGetSerializableFieldSaveFlagMethod(compilation, out var attributeData)) - { - saveFlagMethods.Add((methodSymbol, attributeData)); - } - else if (methodSymbol.TryGetSerializableFieldDefaultMethod(compilation, out attributeData)) - { - defaultMethods.Add((methodSymbol, attributeData)); - } - else if (methodSymbol.TryGetSerializableFieldChangedMethod(compilation, out attributeData)) - { - changedMethods.Add((methodSymbol, attributeData)); - } - } - } } var isValueType = classSymbol.IsValueType; @@ -130,37 +109,6 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a var version = (int)serializableAttr.ConstructorArguments[0].Value!; var encodedVersion = (bool)serializableAttr.ConstructorArguments[1].Value!; - var buildWarnings = new List(); - - // Serializable member names -> (order, type), for name-based linkage. - var memberOrders = new Dictionary(); - - foreach (var (symbol, attributeData) in fields) - { - var order = (int)attributeData.ConstructorArguments[0].Value!; - if (order >= 0 && symbol is IFieldSymbol fieldSymbol) - { - memberOrders[fieldSymbol.Name] = (order, fieldSymbol.Type); - } - } - - foreach (var (symbol, attributeData) in properties) - { - var order = (int)attributeData.ConstructorArguments[0].Value!; - if (order < 0 || symbol is not IPropertySymbol propertySymbol) - { - continue; - } - - memberOrders[propertySymbol.Name] = (order, propertySymbol.Type); - - var useFieldName = (string)attributeData.ConstructorArguments[1].Value!; - if (!string.IsNullOrWhiteSpace(useFieldName)) - { - memberOrders[useFieldName] = (order, propertySymbol.Type); - } - } - static bool IsSaveFlagShape(IMethodSymbol method) => method is { ReturnsVoid: false, Parameters.Length: 0, ReturnType.SpecialType: SpecialType.System_Boolean }; @@ -173,65 +121,6 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, fieldType) && SymbolEqualityComparer.Default.Equals(method.Parameters[1].Type, fieldType); - // Save flag / default-value linkage: method-side attributes name the field; the - // field-side [SaveFlag] names the methods. One style per field, not both. - var serializableFieldSaveFlags = new SortedDictionary(); - - foreach (var (symbol, attrData) in saveFlagMethods) - { - var method = (IMethodSymbol)symbol; - var fieldName = attrData.ConstructorArguments[0].Value as string; - - if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) - { - return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldSaveFlag", fieldName ?? ""); - } - - if (!IsSaveFlagShape(method)) - { - return Fail(DiagnosticDescriptors.SG3015, method.Name, "SerializableFieldSaveFlag", "bool Method()"); - } - - if (serializableFieldSaveFlags.ContainsKey(target.Order)) - { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, target.Order); - } - - serializableFieldSaveFlags[target.Order] = new SerializableFieldSaveFlagMethods - { - DetermineFieldShouldSerialize = method - }; - } - - foreach (var (symbol, attrData) in defaultMethods) - { - var method = (IMethodSymbol)symbol; - var fieldName = attrData.ConstructorArguments[0].Value as string; - - if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) - { - return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldDefault", fieldName ?? ""); - } - - if (!IsDefaultValueShape(method, target.Type)) - { - return Fail(DiagnosticDescriptors.SG3015, method.Name, "SerializableFieldDefault", $"{target.Type} Method()"); - } - - if (!serializableFieldSaveFlags.TryGetValue(target.Order, out var methods)) - { - buildWarnings.Add(DiagnosticInfo.Create(DiagnosticDescriptors.SG3017, typeNode.GetLocation(), fieldName)); - continue; - } - - if (methods.GetFieldDefaultValue != null) - { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, target.Order); - } - - serializableFieldSaveFlags[target.Order] = methods with { GetFieldDefaultValue = method }; - } - // Dirty tracking / MarkDirty resolution. var parentTypeHasEntityTracking = false; if (!isSerializable && dirtyTrackingEntity == null) @@ -277,34 +166,10 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => markDirtyMethod = null; } - // Change callbacks: method-side attributes name the field; field-side [FieldChanged] - // names the method. One style per field, not both. + // Linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the serializable + // members themselves. The named methods must exist with the expected shapes. + var serializableFieldSaveFlags = new SortedDictionary(); var serializableFieldChangedMethods = new Dictionary(); - foreach (var (symbol, attrData) in changedMethods) - { - var method = (IMethodSymbol)symbol; - var fieldName = attrData.ConstructorArguments[0].Value as string; - - if (fieldName == null || !memberOrders.TryGetValue(fieldName, out var target)) - { - return Fail(DiagnosticDescriptors.SG3014, "SerializableFieldChanged", fieldName ?? ""); - } - - if (!IsChangedShape(method, target.Type)) - { - return Fail(DiagnosticDescriptors.SG3010, method.Name, target.Type.ToDisplayString()); - } - - if (serializableFieldChangedMethods.ContainsKey(target.Order)) - { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, target.Order); - } - - serializableFieldChangedMethods[target.Order] = method; - } - - // Field-side linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the - // serializable members themselves. var timerLinks = new Dictionary(); foreach (var (symbol, attributeData) in fields.Concat(properties)) @@ -312,20 +177,17 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => token.ThrowIfCancellationRequested(); var order = (int)attributeData.ConstructorArguments[0].Value!; - if (order < 0 || !memberOrders.TryGetValue(symbol.Name, out var target)) + if (order < 0) { continue; } + var memberType = (symbol as IFieldSymbol)?.Type ?? ((IPropertySymbol)symbol).Type; + foreach (var attr in symbol.GetAttributes()) { if (attr.IsSaveFlag(compilation)) { - if (serializableFieldSaveFlags.ContainsKey(order)) - { - return Fail(DiagnosticDescriptors.SG3016, symbol.Name, "a save flag"); - } - var shouldName = attr.ConstructorArguments[0].Value as string; var shouldMethod = classSymbol.FindLinkedMethod(shouldName, IsSaveFlagShape); if (shouldMethod == null) @@ -337,10 +199,10 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => var defaultName = attr.ConstructorArguments[1].Value as string; if (defaultName != null) { - defaultMethod = classSymbol.FindLinkedMethod(defaultName, m => IsDefaultValueShape(m, target.Type)); + defaultMethod = classSymbol.FindLinkedMethod(defaultName, m => IsDefaultValueShape(m, memberType)); if (defaultMethod == null) { - return Fail(DiagnosticDescriptors.SG3015, defaultName, "SaveFlag", $"{target.Type} Method()"); + return Fail(DiagnosticDescriptors.SG3015, defaultName, "SaveFlag", $"{memberType} Method()"); } } @@ -352,18 +214,13 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => } else if (attr.IsFieldChanged(compilation)) { - if (serializableFieldChangedMethods.ContainsKey(order)) - { - return Fail(DiagnosticDescriptors.SG3016, symbol.Name, "a change callback"); - } - var methodName = attr.ConstructorArguments[0].Value as string; - var method = classSymbol.FindLinkedMethod(methodName, m => IsChangedShape(m, target.Type)); + var method = classSymbol.FindLinkedMethod(methodName, m => IsChangedShape(m, memberType)); if (method == null) { return Fail( DiagnosticDescriptors.SG3015, methodName ?? "", "FieldChanged", - $"void Method({target.Type} oldValue, {target.Type} newValue)" + $"void Method({memberType} oldValue, {memberType} newValue)" ); } @@ -701,6 +558,6 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => location ); - return new SerializationModelResult(model, buildWarnings.ToEquatableArray()); + return new SerializationModelResult(model, System.Array.Empty().ToEquatableArray()); } } diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs index 6a23d9c..49105a5 100644 --- a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs +++ b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs @@ -37,9 +37,6 @@ public static partial class SymbolMetadata public const string DESERIALIZE_TIMER_ATTRIBUTE = "ModernUO.Serialization.DeserializeTimerAttribute"; public const string SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SaveFlagAttribute"; public const string FIELD_CHANGED_ATTRIBUTE = "ModernUO.Serialization.FieldChangedAttribute"; - public const string SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldSaveFlagAttribute"; - public const string SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldDefaultAttribute"; - public const string SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE = "ModernUO.Serialization.SerializableFieldChangedAttribute"; public const string SERIALIZED_PROPERTY_ATTR_ATTRIBUTE = "ModernUO.Serialization.SerializedPropertyAttrAttribute`1"; public const string SORTED_SET_COMPARER_ATTRIBUTE = "ModernUO.Serialization.SortedSetComparerAttribute"; @@ -492,26 +489,6 @@ public bool TryGetDirtyTrackingEntityField(Compilation compilation) => compilation.GetCachedTypeByMetadataName(DIRTY_TRACKING_ENTITY_ATTRIBUTE), out _ ); - - public bool TryGetSerializableFieldSaveFlagMethod( - Compilation compilation, out AttributeData? attributeData - ) => symbol.TryGetMemberWithAttribute( - compilation.GetCachedTypeByMetadataName(SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE), - out attributeData - ); - - public bool TryGetSerializableFieldDefaultMethod( - Compilation compilation, out AttributeData? attributeData - ) => symbol.TryGetMemberWithAttribute( - compilation.GetCachedTypeByMetadataName(SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE), - out attributeData - ); - - public bool TryGetSerializableFieldChangedMethod(Compilation compilation, out AttributeData? attributeData) => - symbol.TryGetMemberWithAttribute( - compilation.GetCachedTypeByMetadataName(SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE), - out attributeData - ); } extension(INamedTypeSymbol symbol) diff --git a/README.md b/README.md index 87e48ba..07a35c8 100644 --- a/README.md +++ b/README.md @@ -303,32 +303,36 @@ Here is a complete example of how we would convert this: ## v4 Linkage and Timers -v4 removes order-based linkage between fields and their companion methods. Attributes that -reference a serializable field take `nameof(_field)` instead of the field's order, and every -linkage can alternatively be declared on the field itself: +v4 removes order-based linkage between fields and their companion methods. Every linkage is +declared on the serializable field itself, naming its companion methods with `nameof()`: ```cs - // Method-side: attributes on the methods name the field. + // Conditional serialization: the first method decides whether the value is written; the + // optional second method supplies the value at load when it was not written. When the + // second method is omitted, the field keeps its default value. [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] private int _charges; - [SerializableFieldSaveFlag(nameof(_charges))] private bool ShouldSerializeCharges() => _charges != 8; - [SerializableFieldDefault(nameof(_charges))] private int ChargesDefaultValue() => 8; - // Field-side: one attribute on the field names the methods. Same generated code. - [SerializableField(0)] - [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] - private int _charges; - - // Change callbacks work the same way, in either style: + // Change callback: invoked by the generated setter after assignment. [SerializableField(1)] [FieldChanged(nameof(OnLevelChanged))] private int _level; + + private void OnLevelChanged(int oldValue, int newValue) + { + } ``` +Because the declaration lives on the field, the old failure modes cannot be written: a +default cannot exist without a save flag, a linkage cannot point at a missing field, and a +field cannot be linked twice. The generator still verifies that each named method exists +with the expected signature (SG3015). + Timers are declared on the field with `[DeserializeTimer]`, replacing `[TimerDrift]` and `[DeserializeTimerField]`: @@ -347,9 +351,10 @@ deadlines; the delay is then negative when the deadline passed during downtime. ### Migrating from v3 -- Replace `[SerializableFieldSaveFlag(order)]`, `[SerializableFieldDefault(order)]`, and - `[SerializableFieldChanged(order)]` with `nameof(_field)` (or move them onto the field as - `[SaveFlag]` / `[FieldChanged]`). These conversions do not change the wire format. +- Replace `[SerializableFieldSaveFlag(order)]` and `[SerializableFieldDefault(order)]` with + `[SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))]` on the field, and + `[SerializableFieldChanged(order)]` with `[FieldChanged(nameof(Method))]` on the field. + These conversions do not change the wire format. - Replace `[TimerDrift]` + `[DeserializeTimerField(order)]` with `[DeserializeTimer(nameof(Method))]` on the timer field. Drifting timers change wire format (delta time to anchored time), so bump the class's `[SerializationGenerator]` version and From 29eb851a2522b3b564991941b0212cda50439bf2 Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:08:41 -0700 Subject: [PATCH 3/5] Pin [SaveFlag] support on [SerializableProperty] members Co-Authored-By: Claude Fable 5 --- .../LinkageTests.cs | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs index 3c4a75f..8f83478 100644 --- a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -56,6 +56,48 @@ public void Delete() { } Assert.Contains("OnLevelChanged(", generatedSource); } + [Fact] + public void SaveFlag_OnSerializableProperty_GeneratesFlag() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class PropertyFlagItem : ISerializable + { + private string _name; + + [SerializableProperty(0, useField: nameof(_name))] + [SaveFlag(nameof(ShouldSerializeName))] + public string Name + { + get => _name; + set => _name = value; + } + + private bool ShouldSerializeName() => _name != null; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Empty(diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); + Assert.NotNull(generatedSource); + Assert.Contains("enum SaveFlag", generatedSource); + Assert.Contains("Name", generatedSource); + Assert.Contains("ShouldSerializeName()", generatedSource); + } + [Fact] public void SaveFlag_MissingMethod_ReportsDiagnostic() { From 869825811fc03d4a50c40908e01d918ed829e76c Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:16:04 -0700 Subject: [PATCH 4/5] Fold the change callback into [SerializableField] as fieldChanged: The callback is invoked by the generated setter, making it a generated-property option like the getter/setter/isVirtual arguments - not a serialization concern. As an argument of [SerializableField] it is unrepresentable on a [SerializableProperty] (whose user-written setter it could never hook), closing the silent-ignore case. The standalone [FieldChanged] attribute, introduced earlier in this unreleased major, is deleted outright. New SG3018 rejects a fieldChanged callback on a field with no generated setter (readonly, or omitted setter), where it could never fire. Snapshot Expected files remain unchanged: emission-identical. Co-Authored-By: Claude Fable 5 --- .../FieldChangedAttribute.cs | 36 -------------- .../SerializableFieldAttribute.cs | 16 ++++++- .../SerializableFieldChangedAttribute.cs | 7 +-- .../LinkageTests.cs | 45 +++++++++++++++--- .../Snapshots/FieldModifiers/Input.cs | 3 +- .../Snapshots/FieldSideLinkage/Input.cs | 3 +- .../Diagnostics/DiagnosticDescriptors.cs | 9 ++++ .../Diagnostics/DiagnosticInfo.cs | 3 +- ...SerializableEntityGeneration.BuildModel.cs | 47 +++++++++++-------- .../SymbolMetadata/SymbolMetadata.UO.cs | 4 -- README.md | 19 ++++---- 11 files changed, 108 insertions(+), 84 deletions(-) delete mode 100644 ModernUO.Serialization.Annotations/FieldChangedAttribute.cs diff --git a/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs deleted file mode 100644 index 5db2482..0000000 --- a/ModernUO.Serialization.Annotations/FieldChangedAttribute.cs +++ /dev/null @@ -1,36 +0,0 @@ -/************************************************************************* - * ModernUO * - * Copyright 2019-2026 - ModernUO Development Team * - * Email: hi@modernuo.com * - * File: FieldChangedAttribute.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; - -namespace ModernUO.Serialization; - -/// -/// Declares the change callback for a serializable field, on the field itself. The named -/// method must have the signature void Method(T oldValue, T newValue) where T is the -/// field's type; it is invoked by the generated setter after assignment. -/// -/// [SerializableField(2)] -/// [FieldChanged(nameof(OnLevelChanged))] -/// private int _level; -/// -/// -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] -public sealed class FieldChangedAttribute : Attribute -{ - public string MethodName { get; } - - public FieldChangedAttribute(string methodName) => MethodName = methodName; -} diff --git a/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs index f4cf244..8abc277 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs @@ -19,7 +19,16 @@ namespace ModernUO.Serialization; /// /// Hints to the source generator that this field should be serialized. -/// The source generator will generate the property entirely +/// The source generator will generate the property entirely. +/// +/// fieldChanged names a change callback with the signature +/// void Method(T oldValue, T newValue) where T is the field's type; it is invoked by +/// the generated setter after assignment: +/// +/// [SerializableField(2, fieldChanged: nameof(OnLevelChanged))] +/// private int _level; +/// +/// /// [AttributeUsage(AttributeTargets.Field)] public sealed class SerializableFieldAttribute : Attribute @@ -28,17 +37,20 @@ public sealed class SerializableFieldAttribute : Attribute public string PropertyGetter { get; } public string? PropertySetter { get; } public bool IsVirtual { get; } + public string? FieldChanged { get; } public SerializableFieldAttribute( int order, string getter = "public", string setter = "public", - bool isVirtual = false + bool isVirtual = false, + string fieldChanged = null ) { Order = order; PropertyGetter = getter; PropertySetter = setter; IsVirtual = isVirtual; + FieldChanged = fieldChanged; } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs index 3b855a9..160e2e7 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs @@ -18,11 +18,12 @@ namespace ModernUO.Serialization; /// -/// Removed in v4. Change callbacks are declared on the serializable field itself: -/// [FieldChanged(nameof(Method))]. This conversion does not change the wire format. +/// Removed in v4. Change callbacks are declared as part of the field's serialization +/// attribute: [SerializableField(order, fieldChanged: nameof(Method))]. This +/// conversion does not change the wire format. /// [AttributeUsage(AttributeTargets.Method)] -[Obsolete("Removed in v4. Declare [FieldChanged(nameof(Method))] on the serializable field instead. The wire format does not change.", true)] +[Obsolete("Removed in v4. Pass fieldChanged: nameof(Method) to [SerializableField] instead. The wire format does not change.", true)] public sealed class SerializableFieldChangedAttribute : Attribute { public SerializableFieldChangedAttribute(string fieldName) diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs index 8f83478..3c940c5 100644 --- a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -4,9 +4,9 @@ namespace ModernUO.Serialization.Generator.Tests; /// -/// Name-based linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the field name -/// their companion methods, and broken linkage must be reported instead of silently -/// generating garbage. +/// Name-based linkage: [SaveFlag], [DeserializeTimer], and the fieldChanged argument of +/// [SerializableField] name their companion methods, and broken linkage must be reported +/// instead of silently generating garbage. /// public class LinkageTests { @@ -31,8 +31,7 @@ public partial class LinkedItem : ISerializable private int ChargesDefaultValue() => 8; - [SerializableField(1)] - [FieldChanged(nameof(OnLevelChanged))] + [SerializableField(1, fieldChanged: nameof(OnLevelChanged))] private int _level; private void OnLevelChanged(int oldValue, int newValue) @@ -207,8 +206,7 @@ namespace Server.TestContent [SerializationGenerator(0)] public partial class WrongChangedItem : ISerializable { - [SerializableField(0)] - [FieldChanged(nameof(OnLevelChanged))] + [SerializableField(0, fieldChanged: nameof(OnLevelChanged))] private int _level; private void OnLevelChanged(int newValue) @@ -228,6 +226,39 @@ public void Delete() { } Assert.Contains(diagnostics, d => d.Id == "SG3015"); } + [Fact] + public void FieldChanged_OnReadonlyField_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class ReadonlyChangedItem : ISerializable + { + [SerializableField(0, fieldChanged: nameof(OnIdChanged))] + private readonly string _id; + + private void OnIdChanged(string oldValue, string newValue) + { + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3018"); + } + [Fact] public void DeserializeTimer_MissingMethod_ReportsDiagnostic() { diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs index 914a0fc..02c7dfa 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs @@ -16,9 +16,8 @@ public partial class FieldModifiersItem : ISerializable [Tidy] private Dictionary _entries; - [SerializableField(2)] + [SerializableField(2, fieldChanged: nameof(OnLevelChanged))] [InvalidateProperties] - [FieldChanged(nameof(OnLevelChanged))] private int _level; private void OnLevelChanged(int oldValue, int newValue) diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs index 9bf9bcc..ad57d62 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs @@ -21,8 +21,7 @@ public partial class FieldLinkedItem : ISerializable private int ChargesDefaultValue() => 8; - [SerializableField(2)] - [FieldChanged(nameof(OnLevelChanged))] + [SerializableField(2, fieldChanged: nameof(OnLevelChanged))] private int _level; private void OnLevelChanged(int oldValue, int newValue) diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs index 86b9916..285f8d9 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -144,6 +144,15 @@ public static class DiagnosticDescriptors true ); + public static readonly DiagnosticDescriptor SG3018 = new( + "SG3018", + "fieldChanged requires a generated setter", + "The fieldChanged callback for '{0}' can never fire because no setter is generated (readonly field or omitted setter)", + "ModernUO.Serialization.Generator", + DiagnosticSeverity.Error, + true + ); + public static DiagnosticDescriptor GeneratorCrashedDiagnostic(Exception e) => new( "SG0001", diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs index 20862ec..7968411 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs @@ -47,7 +47,8 @@ EquatableArray Args ["SG3011"] = DiagnosticDescriptors.SG3011, ["SG3012"] = DiagnosticDescriptors.SG3012, ["SG3013"] = DiagnosticDescriptors.SG3013, - ["SG3015"] = DiagnosticDescriptors.SG3015 + ["SG3015"] = DiagnosticDescriptors.SG3015, + ["SG3018"] = DiagnosticDescriptors.SG3018 }; public static DiagnosticInfo Create(DiagnosticDescriptor descriptor, Location location, params object[] args) diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs index bf5e7a5..04598b5 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs @@ -166,10 +166,9 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => markDirtyMethod = null; } - // Linkage: [SaveFlag], [FieldChanged], and [DeserializeTimer] on the serializable - // members themselves. The named methods must exist with the expected shapes. + // Linkage: [SaveFlag] and [DeserializeTimer] on the serializable members themselves. + // The named methods must exist with the expected shapes. var serializableFieldSaveFlags = new SortedDictionary(); - var serializableFieldChangedMethods = new Dictionary(); var timerLinks = new Dictionary(); foreach (var (symbol, attributeData) in fields.Concat(properties)) @@ -212,20 +211,6 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => GetFieldDefaultValue = defaultMethod }; } - else if (attr.IsFieldChanged(compilation)) - { - var methodName = attr.ConstructorArguments[0].Value as string; - var method = classSymbol.FindLinkedMethod(methodName, m => IsChangedShape(m, memberType)); - if (method == null) - { - return Fail( - DiagnosticDescriptors.SG3015, methodName ?? "", "FieldChanged", - $"void Method({memberType} oldValue, {memberType} newValue)" - ); - } - - serializableFieldChangedMethods[order] = method; - } else if (attr.IsDeserializeTimer(compilation)) { var methodName = attr.ConstructorArguments[0].Value as string; @@ -365,8 +350,32 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => // Readonly fields cannot have setters - force to null var effectiveSetterAccessor = fieldSymbol.IsReadOnly ? (Accessibility?)null : setterAccessor; - // Signatures are validated during linkage resolution. - serializableFieldChangedMethods.TryGetValue(order, out var fieldChangedMethod); + // The change callback is part of [SerializableField] itself, so it cannot be + // declared on a member without a generated setter. + IMethodSymbol? fieldChangedMethod = null; + var fieldChangedName = attrCtorArgs.Length > 4 ? attrCtorArgs[4].Value as string : null; + if (fieldChangedName != null) + { + // The callback is invoked by the generated setter; without one it could + // never fire. + if (effectiveSetterAccessor == null) + { + return Fail(DiagnosticDescriptors.SG3018, fieldSymbol.Name); + } + + fieldChangedMethod = classSymbol.FindLinkedMethod( + fieldChangedName, + m => IsChangedShape(m, fieldSymbol.Type) + ); + + if (fieldChangedMethod == null) + { + return Fail( + DiagnosticDescriptors.SG3015, fieldChangedName, "SerializableField fieldChanged", + $"void Method({fieldSymbol.Type} oldValue, {fieldSymbol.Type} newValue)" + ); + } + } var invalidateProperties = allAttributes.Any( attr => attr.AttributeClass?.Equals( diff --git a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs index 49105a5..70651b0 100644 --- a/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs +++ b/ModernUO.Serialization.Generator/SourceGeneration/SymbolMetadata/SymbolMetadata.UO.cs @@ -36,7 +36,6 @@ public static partial class SymbolMetadata public const string TIDY_ATTRIBUTE = "ModernUO.Serialization.TidyAttribute"; public const string DESERIALIZE_TIMER_ATTRIBUTE = "ModernUO.Serialization.DeserializeTimerAttribute"; public const string SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SaveFlagAttribute"; - public const string FIELD_CHANGED_ATTRIBUTE = "ModernUO.Serialization.FieldChangedAttribute"; public const string SERIALIZED_PROPERTY_ATTR_ATTRIBUTE = "ModernUO.Serialization.SerializedPropertyAttrAttribute`1"; public const string SORTED_SET_COMPARER_ATTRIBUTE = "ModernUO.Serialization.SortedSetComparerAttribute"; @@ -94,9 +93,6 @@ public bool IsDeserializeTimer(Compilation compilation) => public bool IsSaveFlag(Compilation compilation) => attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(SAVE_FLAG_ATTRIBUTE)) == true; - - public bool IsFieldChanged(Compilation compilation) => - attr?.IsAttribute(compilation.GetCachedTypeByMetadataName(FIELD_CHANGED_ATTRIBUTE)) == true; } public static bool IsTimer(this ITypeSymbol symbol, Compilation compilation) => diff --git a/README.md b/README.md index 07a35c8..76d4600 100644 --- a/README.md +++ b/README.md @@ -318,9 +318,10 @@ declared on the serializable field itself, naming its companion methods with `na private int ChargesDefaultValue() => 8; - // Change callback: invoked by the generated setter after assignment. - [SerializableField(1)] - [FieldChanged(nameof(OnLevelChanged))] + // Change callback: invoked by the generated setter after assignment. It is part of + // [SerializableField] itself because it configures the generated property, like the + // getter/setter arguments. + [SerializableField(1, fieldChanged: nameof(OnLevelChanged))] private int _level; private void OnLevelChanged(int oldValue, int newValue) @@ -329,9 +330,11 @@ declared on the serializable field itself, naming its companion methods with `na ``` Because the declaration lives on the field, the old failure modes cannot be written: a -default cannot exist without a save flag, a linkage cannot point at a missing field, and a -field cannot be linked twice. The generator still verifies that each named method exists -with the expected signature (SG3015). +default cannot exist without a save flag, a linkage cannot point at a missing field, a +field cannot be linked twice, and a change callback cannot be declared on a +`[SerializableProperty]` (whose setter is user-written — call your method from the setter). +The generator still verifies that each named method exists with the expected signature +(SG3015), and that a `fieldChanged` callback has a generated setter to fire from (SG3018). Timers are declared on the field with `[DeserializeTimer]`, replacing `[TimerDrift]` and `[DeserializeTimerField]`: @@ -353,8 +356,8 @@ deadlines; the delay is then negative when the deadline passed during downtime. - Replace `[SerializableFieldSaveFlag(order)]` and `[SerializableFieldDefault(order)]` with `[SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))]` on the field, and - `[SerializableFieldChanged(order)]` with `[FieldChanged(nameof(Method))]` on the field. - These conversions do not change the wire format. + `[SerializableFieldChanged(order)]` with `fieldChanged: nameof(Method)` on the field's + `[SerializableField]`. These conversions do not change the wire format. - Replace `[TimerDrift]` + `[DeserializeTimerField(order)]` with `[DeserializeTimer(nameof(Method))]` on the timer field. Drifting timers change wire format (delta time to anchored time), so bump the class's `[SerializationGenerator]` version and From 3408b7766bf13afeb37e37bb543b9c44946ec42f Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Sat, 22 Aug 2026 17:24:23 -0700 Subject: [PATCH 5/5] Add allowFieldChange gate to [SerializableField] bool Method(ref T value), invoked by the generated setter after the equality check and before assignment: it may coerce the incoming value through the ref parameter, and returning false rejects the change. The field still holds the old value while the gate runs, so no oldValue parameter is needed. A survey of all 204 [SerializableProperty] sites in the engine found value coercion (clamps, normalization) to be the single largest reason for hand-written setters; with this gate plus fieldChanged, roughly three quarters of them are expressible as plain [SerializableField] declarations. Hand-written properties remain for custom getters and exotic setter semantics. SG3018 is generalized to cover any setter hook declared without a generated setter. The gate emission is pinned in the FieldSideLinkage snapshot (compile- gated) and unit tests assert the gate -> assign -> fieldChanged pipeline order. Co-Authored-By: Claude Fable 5 --- .../SerializableFieldAttribute.cs | 24 +++- .../LinkageTests.cs | 111 ++++++++++++++++++ ...Content.FieldLinkedItem.Serialization.g.cs | 22 ++++ .../Snapshots/FieldSideLinkage/Input.cs | 13 ++ .../Diagnostics/DiagnosticDescriptors.cs | 4 +- ...SerializableEntityGeneration.BuildModel.cs | 37 +++++- .../SerializableEntityGeneration.Property.cs | 13 ++ .../SerializationModel.cs | 1 + README.md | 23 +++- 9 files changed, 233 insertions(+), 15 deletions(-) diff --git a/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs index 8abc277..5f3c260 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs @@ -21,12 +21,25 @@ namespace ModernUO.Serialization; /// Hints to the source generator that this field should be serialized. /// The source generator will generate the property entirely. /// +/// allowFieldChange names a gate with the signature bool Method(ref T value) +/// where T is the field's type. The generated setter invokes it before assignment (after the +/// equality check); it may coerce the incoming value through the ref parameter, and returning +/// false rejects the change entirely. The field still holds the old value while the gate +/// runs. +/// +/// /// fieldChanged names a change callback with the signature -/// void Method(T oldValue, T newValue) where T is the field's type; it is invoked by -/// the generated setter after assignment: +/// void Method(T oldValue, T newValue); it is invoked by the generated setter after +/// assignment. /// -/// [SerializableField(2, fieldChanged: nameof(OnLevelChanged))] +/// [SerializableField(2, allowFieldChange: nameof(AllowLevelChange), fieldChanged: nameof(OnLevelChanged))] /// private int _level; +/// +/// private bool AllowLevelChange(ref int value) +/// { +/// value = Math.Clamp(value, 0, 100); +/// return true; +/// } /// /// /// @@ -38,13 +51,15 @@ public sealed class SerializableFieldAttribute : Attribute public string? PropertySetter { get; } public bool IsVirtual { get; } public string? FieldChanged { get; } + public string? AllowFieldChange { get; } public SerializableFieldAttribute( int order, string getter = "public", string setter = "public", bool isVirtual = false, - string fieldChanged = null + string fieldChanged = null, + string allowFieldChange = null ) { Order = order; @@ -52,5 +67,6 @@ public SerializableFieldAttribute( PropertySetter = setter; IsVirtual = isVirtual; FieldChanged = fieldChanged; + AllowFieldChange = allowFieldChange; } } diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs index 3c940c5..9a35e7a 100644 --- a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -226,6 +226,117 @@ public void Delete() { } Assert.Contains(diagnostics, d => d.Id == "SG3015"); } + [Fact] + public void AllowFieldChange_GeneratesGateBeforeAssignment() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class GatedItem : ISerializable + { + [SerializableField(0, allowFieldChange: nameof(AllowLevelChange), fieldChanged: nameof(OnLevelChanged))] + private int _level; + + private bool AllowLevelChange(ref int value) + { + value = Math.Clamp(value, 0, 100); + return true; + } + + private void OnLevelChanged(int oldValue, int newValue) + { + } + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Empty(diagnostics.Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)); + Assert.NotNull(generatedSource); + Assert.Contains("if (!AllowLevelChange(ref value))", generatedSource); + + // Pipeline order: gate -> assignment -> changed callback. + var gateIndex = generatedSource.IndexOf("AllowLevelChange(ref value)"); + var assignIndex = generatedSource.IndexOf("_level = value;"); + var changedIndex = generatedSource.IndexOf("OnLevelChanged(oldValue, value);"); + Assert.True(gateIndex >= 0 && assignIndex >= 0 && changedIndex >= 0); + Assert.True(gateIndex < assignIndex, "gate must run before assignment"); + Assert.True(assignIndex < changedIndex, "changed callback must run after assignment"); + } + + [Fact] + public void AllowFieldChange_WrongSignature_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class WrongGateItem : ISerializable + { + [SerializableField(0, allowFieldChange: nameof(AllowLevelChange))] + private int _level; + + private bool AllowLevelChange(int value) => true; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3015"); + } + + [Fact] + public void AllowFieldChange_OnReadonlyField_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class ReadonlyGateItem : ISerializable + { + [SerializableField(0, allowFieldChange: nameof(AllowIdChange))] + private readonly string _id; + + private bool AllowIdChange(ref string value) => true; + + public DateTime Created { get; set; } + public Serial Serial { get; } + public bool Deleted => false; + public void Delete() { } + } + } + """; + + var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); + + Assert.Contains(diagnostics, d => d.Id == "SG3018"); + } + [Fact] public void FieldChanged_OnReadonlyField_ReportsDiagnostic() { diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs index 67380c1..b2f19c3 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs @@ -56,6 +56,24 @@ public int Level } } + public int Water + { + get => _water; + set + { + if (value != _water) + { + if (!AllowWaterChange(ref value)) + { + return; + } + + _water = value; + Server.ISerializableExtensions.MarkDirty(this); + } + } + } + public FieldLinkedItem(Server.Serial serial) { Serial = serial; @@ -87,6 +105,8 @@ public virtual void Serialize(Server.IGenericWriter writer) } writer.Write(_level); + + writer.Write(_water); } public virtual void Deserialize(Server.IGenericReader reader) @@ -110,6 +130,8 @@ public virtual void Deserialize(Server.IGenericReader reader) } _level = reader.ReadInt(); + + _water = reader.ReadInt(); } [System.Flags] diff --git a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs index ad57d62..7a3f866 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs @@ -28,6 +28,19 @@ private void OnLevelChanged(int oldValue, int newValue) { } + [SerializableField(3, allowFieldChange: nameof(AllowWaterChange))] + private int _water; + + private bool AllowWaterChange(ref int value) + { + if (value < 0) + { + value = 0; + } + + return value <= 100; + } + public DateTime Created { get; set; } public Serial Serial { get; } public bool Deleted => false; diff --git a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs index 285f8d9..c94d10b 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticDescriptors.cs @@ -146,8 +146,8 @@ public static class DiagnosticDescriptors public static readonly DiagnosticDescriptor SG3018 = new( "SG3018", - "fieldChanged requires a generated setter", - "The fieldChanged callback for '{0}' can never fire because no setter is generated (readonly field or omitted setter)", + "Setter hook requires a generated setter", + "The {0} method for '{1}' can never fire because no setter is generated (readonly field or omitted setter)", "ModernUO.Serialization.Generator", DiagnosticSeverity.Error, true diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs index 04598b5..9dd6d5f 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.BuildModel.cs @@ -121,6 +121,11 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, fieldType) && SymbolEqualityComparer.Default.Equals(method.Parameters[1].Type, fieldType); + static bool IsAllowChangeShape(IMethodSymbol method, ITypeSymbol fieldType) => + method is { ReturnsVoid: false, Parameters.Length: 1, ReturnType.SpecialType: SpecialType.System_Boolean } && + method.Parameters[0].RefKind == RefKind.Ref && + SymbolEqualityComparer.Default.Equals(method.Parameters[0].Type, fieldType); + // Dirty tracking / MarkDirty resolution. var parentTypeHasEntityTracking = false; if (!isSerializable && dirtyTrackingEntity == null) @@ -350,17 +355,15 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => // Readonly fields cannot have setters - force to null var effectiveSetterAccessor = fieldSymbol.IsReadOnly ? (Accessibility?)null : setterAccessor; - // The change callback is part of [SerializableField] itself, so it cannot be - // declared on a member without a generated setter. + // The setter hooks are part of [SerializableField] itself, so they cannot be + // declared on a member without a generated setter to invoke them. IMethodSymbol? fieldChangedMethod = null; var fieldChangedName = attrCtorArgs.Length > 4 ? attrCtorArgs[4].Value as string : null; if (fieldChangedName != null) { - // The callback is invoked by the generated setter; without one it could - // never fire. if (effectiveSetterAccessor == null) { - return Fail(DiagnosticDescriptors.SG3018, fieldSymbol.Name); + return Fail(DiagnosticDescriptors.SG3018, "fieldChanged", fieldSymbol.Name); } fieldChangedMethod = classSymbol.FindLinkedMethod( @@ -377,6 +380,29 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => } } + IMethodSymbol? allowFieldChangeMethod = null; + var allowFieldChangeName = attrCtorArgs.Length > 5 ? attrCtorArgs[5].Value as string : null; + if (allowFieldChangeName != null) + { + if (effectiveSetterAccessor == null) + { + return Fail(DiagnosticDescriptors.SG3018, "allowFieldChange", fieldSymbol.Name); + } + + allowFieldChangeMethod = classSymbol.FindLinkedMethod( + allowFieldChangeName, + m => IsAllowChangeShape(m, fieldSymbol.Type) + ); + + if (allowFieldChangeMethod == null) + { + return Fail( + DiagnosticDescriptors.SG3015, allowFieldChangeName, "SerializableField allowFieldChange", + $"bool Method(ref {fieldSymbol.Type} value)" + ); + } + } + var invalidateProperties = allAttributes.Any( attr => attr.AttributeClass?.Equals( compilation.GetCachedTypeByMetadataName(SymbolMetadata.INVALIDATE_PROPERTIES_ATTRIBUTE), @@ -420,6 +446,7 @@ static bool IsChangedShape(IMethodSymbol method, ITypeSymbol fieldType) => fieldSymbol.Type.HasInequalityOperator(), invalidateProperties, fieldChangedMethod?.Name, + allowFieldChangeMethod?.Name, attributeLines.ToEquatableArray(), dsIsArray, dsIsDictionary, diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs index 3f7a1e8..d6f8d6e 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializableEntityGeneration.Property.cs @@ -60,6 +60,19 @@ public static void GenerateSerializableProperty( source.AppendLine($"{innerIndent}if ({comparison})"); source.AppendLine($"{innerIndent}{{"); + + // The gate runs before assignment: it may coerce `value` through the ref + // parameter or return false to reject the change. The field still holds the + // old value while it executes. + if (field.AllowFieldChangeMethodName != null) + { + source.AppendLine($"{innerIndent} if (!{field.AllowFieldChangeMethodName}(ref value))"); + source.AppendLine($"{innerIndent} {{"); + source.AppendLine($"{innerIndent} return;"); + source.AppendLine($"{innerIndent} }}"); + source.AppendLine(); + } + source.AppendLine($"{innerIndent} {fieldName} = value;"); if (markDirtyMethod != null) { diff --git a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs index b76efd9..56a643f 100644 --- a/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs +++ b/ModernUO.Serialization.Generator/SerializableEntityGeneration/SerializationModel.cs @@ -126,6 +126,7 @@ public sealed record FieldPropertyModel( bool HasInequalityOperator, bool InvalidateProperties, string? FieldChangedMethodName, + string? AllowFieldChangeMethodName, EquatableArray AttributeLines, bool DsIsArray, bool DsIsDictionary, diff --git a/README.md b/README.md index 76d4600..234ce28 100644 --- a/README.md +++ b/README.md @@ -318,17 +318,32 @@ declared on the serializable field itself, naming its companion methods with `na private int ChargesDefaultValue() => 8; - // Change callback: invoked by the generated setter after assignment. It is part of - // [SerializableField] itself because it configures the generated property, like the - // getter/setter arguments. - [SerializableField(1, fieldChanged: nameof(OnLevelChanged))] + // Setter hooks are part of [SerializableField] itself because they configure the + // generated property, like the getter/setter arguments. + // + // allowFieldChange runs before assignment (after the equality check): it may coerce the + // incoming value through the ref parameter, and returning false rejects the change. The + // field still holds the old value while it runs. fieldChanged runs after assignment. + [SerializableField(1, allowFieldChange: nameof(AllowLevelChange), fieldChanged: nameof(OnLevelChanged))] private int _level; + private bool AllowLevelChange(ref int value) + { + value = Math.Clamp(value, 0, 100); + return true; + } + private void OnLevelChanged(int oldValue, int newValue) { } ``` +The generated setter pipeline is: equality check → `allowFieldChange` (coerce/veto) → +assignment → dirty tracking → `fieldChanged`. Between these hooks, most hand-written +`[SerializableProperty]` setters (clamps, normalization, guarded rejection, post-change side +effects) can be expressed as a plain `[SerializableField]`; hand-written properties remain +for custom *getters* and truly exotic setters. + Because the declaration lives on the field, the old failure modes cannot be written: a default cannot exist without a save flag, a linkage cannot point at a missing field, a field cannot be linked twice, and a change callback cannot be declared on a