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/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..cb54e59 --- /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; +/// +/// 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 +{ + 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/SerializableFieldAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs index f4cf244..5f3c260 100644 --- a/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs +++ b/ModernUO.Serialization.Annotations/SerializableFieldAttribute.cs @@ -19,7 +19,29 @@ 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. +/// +/// 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); it is invoked by the generated setter after +/// assignment. +/// +/// [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; +/// } +/// +/// /// [AttributeUsage(AttributeTargets.Field)] public sealed class SerializableFieldAttribute : Attribute @@ -28,17 +50,23 @@ public sealed class SerializableFieldAttribute : Attribute public string PropertyGetter { get; } 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 + bool isVirtual = false, + string fieldChanged = null, + string allowFieldChange = null ) { Order = order; PropertyGetter = getter; PropertySetter = setter; IsVirtual = isVirtual; + FieldChanged = fieldChanged; + AllowFieldChange = allowFieldChange; } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldChangedAttribute.cs index 60b8e7a..160e2e7 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,19 @@ 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. +/// 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. Pass fieldChanged: nameof(Method) to [SerializableField] instead. The wire format does not change.", true)] public sealed class SerializableFieldChangedAttribute : Attribute { - public int Order { get; } + public SerializableFieldChangedAttribute(string fieldName) + { + } - public SerializableFieldChangedAttribute(int order) => Order = order; + public SerializableFieldChangedAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldDefaultAttribute.cs index c686bce..5562937 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,19 @@ 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. -/// -/// 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. +/// 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 int Order { get; } + public SerializableFieldDefaultAttribute(string fieldName) + { + } - public SerializableFieldDefaultAttribute(int order) => Order = order; + public SerializableFieldDefaultAttribute(int order) + { + } } diff --git a/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs b/ModernUO.Serialization.Annotations/SerializableFieldSaveFlagAttribute.cs index c198ac8..12a8fd8 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,19 @@ namespace ModernUO.Serialization; /// -/// Hints to the source generator that the field with the same order value should use a save flag. +/// 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 int Order { get; } + public SerializableFieldSaveFlagAttribute(string fieldName) + { + } - public SerializableFieldSaveFlagAttribute(int order) => Order = order; + 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..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(0)] - private bool ShouldSerializeName() => _name != null; - - [SerializableFieldSaveFlag(0)] - 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 NegativeSaveFlagOrder_ReportsDiagnostic() - { - const string source = """ - using ModernUO.Serialization; - using Server; - - namespace TestNamespace - { - [SerializationGenerator(0)] - public partial class NegativeFlagItem : ISerializable - { - [SerializableField(0)] - private string _name; - - [SerializableFieldSaveFlag(-1)] - private bool ShouldSerializeName() => _name != null; - - public Serial Serial => default; - public void MarkDirty() { } - } - } - """; - - var (diagnostics, _) = SourceGeneratorTestHelper.RunGenerator(source); - - Assert.Contains(diagnostics, d => d.Id == "SG3006"); - } } diff --git a/ModernUO.Serialization.Generator.Tests/LinkageTests.cs b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs new file mode 100644 index 0000000..9a35e7a --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/LinkageTests.cs @@ -0,0 +1,431 @@ +using ModernUO.Serialization.Generator.Tests.Helpers; +using Xunit; + +namespace ModernUO.Serialization.Generator.Tests; + +/// +/// 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 +{ + [Fact] + public void FieldSideLinkage_GeneratesLinkedCode() + { + const string source = """ + 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() { } + } + } + """; + + var (diagnostics, generatedSource) = SourceGeneratorTestHelper.RunGenerator(source); + + 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 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() + { + 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 SaveFlag_WrongSignature_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class WrongShapeItem : ISerializable + { + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges))] + private int _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 SaveFlag_DefaultWrongSignature_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class WrongDefaultItem : ISerializable + { + [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] + private int _charges; + + private bool ShouldSerializeCharges() => _charges != 8; + + private string ChargesDefaultValue() => "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 == "SG3015"); + } + + [Fact] + public void FieldChanged_WrongSignature_ReportsDiagnostic() + { + const string source = """ + using System; + using ModernUO.Serialization; + using Server; + + namespace Server.TestContent + { + [SerializationGenerator(0)] + public partial class WrongChangedItem : ISerializable + { + [SerializableField(0, fieldChanged: nameof(OnLevelChanged))] + private int _level; + + private void OnLevelChanged(int 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 == "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() + { + 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() + { + 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..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(0)] private bool ShouldSerializeId() => _id != null; - [SerializableFieldSaveFlag(1)] 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..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(0)] 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({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({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({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(0)] private bool ShouldSerializeCount() => _count != 0; - [SerializableFieldDefault(0)] 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..02c7dfa 100644 --- a/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldModifiers/Input.cs @@ -16,11 +16,10 @@ public partial class FieldModifiersItem : ISerializable [Tidy] private Dictionary _entries; - [SerializableField(2)] + [SerializableField(2, fieldChanged: nameof(OnLevelChanged))] [InvalidateProperties] private int _level; - [SerializableFieldChanged(2)] 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..b2f19c3 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Expected/Server.TestContent.FieldLinkedItem.Serialization.g.cs @@ -0,0 +1,145 @@ +// +// 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 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; + } + + 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); + + writer.Write(_water); + } + + 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(); + + _water = 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..7a3f866 --- /dev/null +++ b/ModernUO.Serialization.Generator.Tests/Snapshots/FieldSideLinkage/Input.cs @@ -0,0 +1,49 @@ +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) + { + } + + [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; + 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..b27147b 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; @@ -8,426 +7,426 @@ namespace Server.TestContent public partial class MultiEnumFlagsItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeField0))] private string _field0; - [SerializableFieldSaveFlag(0)] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeField1))] private string _field1; - [SerializableFieldSaveFlag(1)] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeField2))] private string _field2; - [SerializableFieldSaveFlag(2)] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] + [SaveFlag(nameof(ShouldSerializeField3))] private string _field3; - [SerializableFieldSaveFlag(3)] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] + [SaveFlag(nameof(ShouldSerializeField4))] private string _field4; - [SerializableFieldSaveFlag(4)] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] + [SaveFlag(nameof(ShouldSerializeField5))] private string _field5; - [SerializableFieldSaveFlag(5)] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] + [SaveFlag(nameof(ShouldSerializeField6))] private string _field6; - [SerializableFieldSaveFlag(6)] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] + [SaveFlag(nameof(ShouldSerializeField7))] private string _field7; - [SerializableFieldSaveFlag(7)] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] + [SaveFlag(nameof(ShouldSerializeField8))] private string _field8; - [SerializableFieldSaveFlag(8)] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] + [SaveFlag(nameof(ShouldSerializeField9))] private string _field9; - [SerializableFieldSaveFlag(9)] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] + [SaveFlag(nameof(ShouldSerializeField10))] private string _field10; - [SerializableFieldSaveFlag(10)] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] + [SaveFlag(nameof(ShouldSerializeField11))] private string _field11; - [SerializableFieldSaveFlag(11)] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] + [SaveFlag(nameof(ShouldSerializeField12))] private string _field12; - [SerializableFieldSaveFlag(12)] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] + [SaveFlag(nameof(ShouldSerializeField13))] private string _field13; - [SerializableFieldSaveFlag(13)] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] + [SaveFlag(nameof(ShouldSerializeField14))] private string _field14; - [SerializableFieldSaveFlag(14)] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] + [SaveFlag(nameof(ShouldSerializeField15))] private string _field15; - [SerializableFieldSaveFlag(15)] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] + [SaveFlag(nameof(ShouldSerializeField16))] private string _field16; - [SerializableFieldSaveFlag(16)] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] + [SaveFlag(nameof(ShouldSerializeField17))] private string _field17; - [SerializableFieldSaveFlag(17)] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] + [SaveFlag(nameof(ShouldSerializeField18))] private string _field18; - [SerializableFieldSaveFlag(18)] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] + [SaveFlag(nameof(ShouldSerializeField19))] private string _field19; - [SerializableFieldSaveFlag(19)] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] + [SaveFlag(nameof(ShouldSerializeField20))] private string _field20; - [SerializableFieldSaveFlag(20)] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] + [SaveFlag(nameof(ShouldSerializeField21))] private string _field21; - [SerializableFieldSaveFlag(21)] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] + [SaveFlag(nameof(ShouldSerializeField22))] private string _field22; - [SerializableFieldSaveFlag(22)] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] + [SaveFlag(nameof(ShouldSerializeField23))] private string _field23; - [SerializableFieldSaveFlag(23)] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] + [SaveFlag(nameof(ShouldSerializeField24))] private string _field24; - [SerializableFieldSaveFlag(24)] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] + [SaveFlag(nameof(ShouldSerializeField25))] private string _field25; - [SerializableFieldSaveFlag(25)] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] + [SaveFlag(nameof(ShouldSerializeField26))] private string _field26; - [SerializableFieldSaveFlag(26)] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] + [SaveFlag(nameof(ShouldSerializeField27))] private string _field27; - [SerializableFieldSaveFlag(27)] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] + [SaveFlag(nameof(ShouldSerializeField28))] private string _field28; - [SerializableFieldSaveFlag(28)] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] + [SaveFlag(nameof(ShouldSerializeField29))] private string _field29; - [SerializableFieldSaveFlag(29)] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] + [SaveFlag(nameof(ShouldSerializeField30))] private string _field30; - [SerializableFieldSaveFlag(30)] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] + [SaveFlag(nameof(ShouldSerializeField31))] private string _field31; - [SerializableFieldSaveFlag(31)] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] + [SaveFlag(nameof(ShouldSerializeField32))] private string _field32; - [SerializableFieldSaveFlag(32)] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] + [SaveFlag(nameof(ShouldSerializeField33))] private string _field33; - [SerializableFieldSaveFlag(33)] private bool ShouldSerializeField33() => _field33 != null; [SerializableField(34)] + [SaveFlag(nameof(ShouldSerializeField34))] private string _field34; - [SerializableFieldSaveFlag(34)] private bool ShouldSerializeField34() => _field34 != null; [SerializableField(35)] + [SaveFlag(nameof(ShouldSerializeField35))] private string _field35; - [SerializableFieldSaveFlag(35)] private bool ShouldSerializeField35() => _field35 != null; [SerializableField(36)] + [SaveFlag(nameof(ShouldSerializeField36))] private string _field36; - [SerializableFieldSaveFlag(36)] private bool ShouldSerializeField36() => _field36 != null; [SerializableField(37)] + [SaveFlag(nameof(ShouldSerializeField37))] private string _field37; - [SerializableFieldSaveFlag(37)] private bool ShouldSerializeField37() => _field37 != null; [SerializableField(38)] + [SaveFlag(nameof(ShouldSerializeField38))] private string _field38; - [SerializableFieldSaveFlag(38)] private bool ShouldSerializeField38() => _field38 != null; [SerializableField(39)] + [SaveFlag(nameof(ShouldSerializeField39))] private string _field39; - [SerializableFieldSaveFlag(39)] private bool ShouldSerializeField39() => _field39 != null; [SerializableField(40)] + [SaveFlag(nameof(ShouldSerializeField40))] private string _field40; - [SerializableFieldSaveFlag(40)] private bool ShouldSerializeField40() => _field40 != null; [SerializableField(41)] + [SaveFlag(nameof(ShouldSerializeField41))] private string _field41; - [SerializableFieldSaveFlag(41)] private bool ShouldSerializeField41() => _field41 != null; [SerializableField(42)] + [SaveFlag(nameof(ShouldSerializeField42))] private string _field42; - [SerializableFieldSaveFlag(42)] private bool ShouldSerializeField42() => _field42 != null; [SerializableField(43)] + [SaveFlag(nameof(ShouldSerializeField43))] private string _field43; - [SerializableFieldSaveFlag(43)] private bool ShouldSerializeField43() => _field43 != null; [SerializableField(44)] + [SaveFlag(nameof(ShouldSerializeField44))] private string _field44; - [SerializableFieldSaveFlag(44)] private bool ShouldSerializeField44() => _field44 != null; [SerializableField(45)] + [SaveFlag(nameof(ShouldSerializeField45))] private string _field45; - [SerializableFieldSaveFlag(45)] private bool ShouldSerializeField45() => _field45 != null; [SerializableField(46)] + [SaveFlag(nameof(ShouldSerializeField46))] private string _field46; - [SerializableFieldSaveFlag(46)] private bool ShouldSerializeField46() => _field46 != null; [SerializableField(47)] + [SaveFlag(nameof(ShouldSerializeField47))] private string _field47; - [SerializableFieldSaveFlag(47)] private bool ShouldSerializeField47() => _field47 != null; [SerializableField(48)] + [SaveFlag(nameof(ShouldSerializeField48))] private string _field48; - [SerializableFieldSaveFlag(48)] private bool ShouldSerializeField48() => _field48 != null; [SerializableField(49)] + [SaveFlag(nameof(ShouldSerializeField49))] private string _field49; - [SerializableFieldSaveFlag(49)] private bool ShouldSerializeField49() => _field49 != null; [SerializableField(50)] + [SaveFlag(nameof(ShouldSerializeField50))] private string _field50; - [SerializableFieldSaveFlag(50)] private bool ShouldSerializeField50() => _field50 != null; [SerializableField(51)] + [SaveFlag(nameof(ShouldSerializeField51))] private string _field51; - [SerializableFieldSaveFlag(51)] private bool ShouldSerializeField51() => _field51 != null; [SerializableField(52)] + [SaveFlag(nameof(ShouldSerializeField52))] private string _field52; - [SerializableFieldSaveFlag(52)] private bool ShouldSerializeField52() => _field52 != null; [SerializableField(53)] + [SaveFlag(nameof(ShouldSerializeField53))] private string _field53; - [SerializableFieldSaveFlag(53)] private bool ShouldSerializeField53() => _field53 != null; [SerializableField(54)] + [SaveFlag(nameof(ShouldSerializeField54))] private string _field54; - [SerializableFieldSaveFlag(54)] private bool ShouldSerializeField54() => _field54 != null; [SerializableField(55)] + [SaveFlag(nameof(ShouldSerializeField55))] private string _field55; - [SerializableFieldSaveFlag(55)] private bool ShouldSerializeField55() => _field55 != null; [SerializableField(56)] + [SaveFlag(nameof(ShouldSerializeField56))] private string _field56; - [SerializableFieldSaveFlag(56)] private bool ShouldSerializeField56() => _field56 != null; [SerializableField(57)] + [SaveFlag(nameof(ShouldSerializeField57))] private string _field57; - [SerializableFieldSaveFlag(57)] private bool ShouldSerializeField57() => _field57 != null; [SerializableField(58)] + [SaveFlag(nameof(ShouldSerializeField58))] private string _field58; - [SerializableFieldSaveFlag(58)] private bool ShouldSerializeField58() => _field58 != null; [SerializableField(59)] + [SaveFlag(nameof(ShouldSerializeField59))] private string _field59; - [SerializableFieldSaveFlag(59)] private bool ShouldSerializeField59() => _field59 != null; [SerializableField(60)] + [SaveFlag(nameof(ShouldSerializeField60))] private string _field60; - [SerializableFieldSaveFlag(60)] private bool ShouldSerializeField60() => _field60 != null; [SerializableField(61)] + [SaveFlag(nameof(ShouldSerializeField61))] private string _field61; - [SerializableFieldSaveFlag(61)] private bool ShouldSerializeField61() => _field61 != null; [SerializableField(62)] + [SaveFlag(nameof(ShouldSerializeField62))] private string _field62; - [SerializableFieldSaveFlag(62)] private bool ShouldSerializeField62() => _field62 != null; [SerializableField(63)] + [SaveFlag(nameof(ShouldSerializeField63))] private string _field63; - [SerializableFieldSaveFlag(63)] private bool ShouldSerializeField63() => _field63 != null; [SerializableField(64)] + [SaveFlag(nameof(ShouldSerializeField64))] private string _field64; - [SerializableFieldSaveFlag(64)] private bool ShouldSerializeField64() => _field64 != null; [SerializableField(65)] + [SaveFlag(nameof(ShouldSerializeField65))] private string _field65; - [SerializableFieldSaveFlag(65)] private bool ShouldSerializeField65() => _field65 != null; [SerializableField(66)] + [SaveFlag(nameof(ShouldSerializeField66))] private string _field66; - [SerializableFieldSaveFlag(66)] private bool ShouldSerializeField66() => _field66 != null; [SerializableField(67)] + [SaveFlag(nameof(ShouldSerializeField67))] private string _field67; - [SerializableFieldSaveFlag(67)] private bool ShouldSerializeField67() => _field67 != null; [SerializableField(68)] + [SaveFlag(nameof(ShouldSerializeField68))] private string _field68; - [SerializableFieldSaveFlag(68)] private bool ShouldSerializeField68() => _field68 != null; [SerializableField(69)] + [SaveFlag(nameof(ShouldSerializeField69))] private string _field69; - [SerializableFieldSaveFlag(69)] 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..31560ec 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; @@ -8,210 +7,210 @@ namespace Server.TestContent public partial class UlongFlagsItem : ISerializable { [SerializableField(0)] + [SaveFlag(nameof(ShouldSerializeField0))] private string _field0; - [SerializableFieldSaveFlag(0)] private bool ShouldSerializeField0() => _field0 != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeField1))] private string _field1; - [SerializableFieldSaveFlag(1)] private bool ShouldSerializeField1() => _field1 != null; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeField2))] private string _field2; - [SerializableFieldSaveFlag(2)] private bool ShouldSerializeField2() => _field2 != null; [SerializableField(3)] + [SaveFlag(nameof(ShouldSerializeField3))] private string _field3; - [SerializableFieldSaveFlag(3)] private bool ShouldSerializeField3() => _field3 != null; [SerializableField(4)] + [SaveFlag(nameof(ShouldSerializeField4))] private string _field4; - [SerializableFieldSaveFlag(4)] private bool ShouldSerializeField4() => _field4 != null; [SerializableField(5)] + [SaveFlag(nameof(ShouldSerializeField5))] private string _field5; - [SerializableFieldSaveFlag(5)] private bool ShouldSerializeField5() => _field5 != null; [SerializableField(6)] + [SaveFlag(nameof(ShouldSerializeField6))] private string _field6; - [SerializableFieldSaveFlag(6)] private bool ShouldSerializeField6() => _field6 != null; [SerializableField(7)] + [SaveFlag(nameof(ShouldSerializeField7))] private string _field7; - [SerializableFieldSaveFlag(7)] private bool ShouldSerializeField7() => _field7 != null; [SerializableField(8)] + [SaveFlag(nameof(ShouldSerializeField8))] private string _field8; - [SerializableFieldSaveFlag(8)] private bool ShouldSerializeField8() => _field8 != null; [SerializableField(9)] + [SaveFlag(nameof(ShouldSerializeField9))] private string _field9; - [SerializableFieldSaveFlag(9)] private bool ShouldSerializeField9() => _field9 != null; [SerializableField(10)] + [SaveFlag(nameof(ShouldSerializeField10))] private string _field10; - [SerializableFieldSaveFlag(10)] private bool ShouldSerializeField10() => _field10 != null; [SerializableField(11)] + [SaveFlag(nameof(ShouldSerializeField11))] private string _field11; - [SerializableFieldSaveFlag(11)] private bool ShouldSerializeField11() => _field11 != null; [SerializableField(12)] + [SaveFlag(nameof(ShouldSerializeField12))] private string _field12; - [SerializableFieldSaveFlag(12)] private bool ShouldSerializeField12() => _field12 != null; [SerializableField(13)] + [SaveFlag(nameof(ShouldSerializeField13))] private string _field13; - [SerializableFieldSaveFlag(13)] private bool ShouldSerializeField13() => _field13 != null; [SerializableField(14)] + [SaveFlag(nameof(ShouldSerializeField14))] private string _field14; - [SerializableFieldSaveFlag(14)] private bool ShouldSerializeField14() => _field14 != null; [SerializableField(15)] + [SaveFlag(nameof(ShouldSerializeField15))] private string _field15; - [SerializableFieldSaveFlag(15)] private bool ShouldSerializeField15() => _field15 != null; [SerializableField(16)] + [SaveFlag(nameof(ShouldSerializeField16))] private string _field16; - [SerializableFieldSaveFlag(16)] private bool ShouldSerializeField16() => _field16 != null; [SerializableField(17)] + [SaveFlag(nameof(ShouldSerializeField17))] private string _field17; - [SerializableFieldSaveFlag(17)] private bool ShouldSerializeField17() => _field17 != null; [SerializableField(18)] + [SaveFlag(nameof(ShouldSerializeField18))] private string _field18; - [SerializableFieldSaveFlag(18)] private bool ShouldSerializeField18() => _field18 != null; [SerializableField(19)] + [SaveFlag(nameof(ShouldSerializeField19))] private string _field19; - [SerializableFieldSaveFlag(19)] private bool ShouldSerializeField19() => _field19 != null; [SerializableField(20)] + [SaveFlag(nameof(ShouldSerializeField20))] private string _field20; - [SerializableFieldSaveFlag(20)] private bool ShouldSerializeField20() => _field20 != null; [SerializableField(21)] + [SaveFlag(nameof(ShouldSerializeField21))] private string _field21; - [SerializableFieldSaveFlag(21)] private bool ShouldSerializeField21() => _field21 != null; [SerializableField(22)] + [SaveFlag(nameof(ShouldSerializeField22))] private string _field22; - [SerializableFieldSaveFlag(22)] private bool ShouldSerializeField22() => _field22 != null; [SerializableField(23)] + [SaveFlag(nameof(ShouldSerializeField23))] private string _field23; - [SerializableFieldSaveFlag(23)] private bool ShouldSerializeField23() => _field23 != null; [SerializableField(24)] + [SaveFlag(nameof(ShouldSerializeField24))] private string _field24; - [SerializableFieldSaveFlag(24)] private bool ShouldSerializeField24() => _field24 != null; [SerializableField(25)] + [SaveFlag(nameof(ShouldSerializeField25))] private string _field25; - [SerializableFieldSaveFlag(25)] private bool ShouldSerializeField25() => _field25 != null; [SerializableField(26)] + [SaveFlag(nameof(ShouldSerializeField26))] private string _field26; - [SerializableFieldSaveFlag(26)] private bool ShouldSerializeField26() => _field26 != null; [SerializableField(27)] + [SaveFlag(nameof(ShouldSerializeField27))] private string _field27; - [SerializableFieldSaveFlag(27)] private bool ShouldSerializeField27() => _field27 != null; [SerializableField(28)] + [SaveFlag(nameof(ShouldSerializeField28))] private string _field28; - [SerializableFieldSaveFlag(28)] private bool ShouldSerializeField28() => _field28 != null; [SerializableField(29)] + [SaveFlag(nameof(ShouldSerializeField29))] private string _field29; - [SerializableFieldSaveFlag(29)] private bool ShouldSerializeField29() => _field29 != null; [SerializableField(30)] + [SaveFlag(nameof(ShouldSerializeField30))] private string _field30; - [SerializableFieldSaveFlag(30)] private bool ShouldSerializeField30() => _field30 != null; [SerializableField(31)] + [SaveFlag(nameof(ShouldSerializeField31))] private string _field31; - [SerializableFieldSaveFlag(31)] private bool ShouldSerializeField31() => _field31 != null; [SerializableField(32)] + [SaveFlag(nameof(ShouldSerializeField32))] private string _field32; - [SerializableFieldSaveFlag(32)] private bool ShouldSerializeField32() => _field32 != null; [SerializableField(33)] + [SaveFlag(nameof(ShouldSerializeField33))] private string _field33; - [SerializableFieldSaveFlag(33)] 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..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(0)] private bool ShouldSerializeName() => _name != null; [SerializableField(1)] + [SaveFlag(nameof(ShouldSerializeCharges), nameof(ChargesDefaultValue))] private int _charges; - [SerializableFieldSaveFlag(1)] private bool ShouldSerializeCharges() => _charges != 8; - [SerializableFieldDefault(1)] private int ChargesDefaultValue() => 8; [SerializableField(2)] + [SaveFlag(nameof(ShouldSerializeExpires), nameof(ExpiresDefaultValue))] private DateTime _expires; - [SerializableFieldSaveFlag(2)] private bool ShouldSerializeExpires() => _expires != DateTime.MinValue; - [SerializableFieldDefault(2)] 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..c94d10b 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 @@ -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,6 +135,24 @@ public static class DiagnosticDescriptors 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 SG3018 = new( + "SG3018", + "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 + ); + 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..7968411 100644 --- a/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs +++ b/ModernUO.Serialization.Generator/Diagnostics/DiagnosticInfo.cs @@ -44,10 +44,11 @@ EquatableArray Args ["SG3007"] = DiagnosticDescriptors.SG3007, ["SG3008"] = DiagnosticDescriptors.SG3008, ["SG3009"] = DiagnosticDescriptors.SG3009, - ["SG3010"] = DiagnosticDescriptors.SG3010, ["SG3011"] = DiagnosticDescriptors.SG3011, ["SG3012"] = DiagnosticDescriptors.SG3012, - ["SG3013"] = DiagnosticDescriptors.SG3013 + ["SG3013"] = DiagnosticDescriptors.SG3013, + ["SG3015"] = DiagnosticDescriptors.SG3015, + ["SG3018"] = DiagnosticDescriptors.SG3018 }; 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..9dd6d5f 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,50 +109,22 @@ 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 serializableFieldSaveFlags = new SortedDictionary(); - foreach (var (symbol, attrData) in saveFlagMethods) - { - var order = (int)attrData.ConstructorArguments[0].Value!; + static bool IsSaveFlagShape(IMethodSymbol method) => + method is { ReturnsVoid: false, Parameters.Length: 0, ReturnType.SpecialType: SpecialType.System_Boolean }; - if (order < 0) - { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, symbol.Name); - } + static bool IsDefaultValueShape(IMethodSymbol method, ITypeSymbol fieldType) => + method is { ReturnsVoid: false, Parameters.Length: 0 } && + SymbolEqualityComparer.Default.Equals(method.ReturnType, fieldType); - if (serializableFieldSaveFlags.ContainsKey(order)) - { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_SAVE_FLAG_ATTRIBUTE, order); - } + 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); - serializableFieldSaveFlags[order] = new SerializableFieldSaveFlagMethods - { - DetermineFieldShouldSerialize = (IMethodSymbol)symbol - }; - } - - foreach (var (symbol, attrData) in defaultMethods) - { - var order = (int)attrData.ConstructorArguments[0].Value!; - - if (order < 0) - { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, symbol.Name); - } - - // No save flag, so we ignore the default value - if (!serializableFieldSaveFlags.TryGetValue(order, out var methods)) - { - continue; - } - - if (methods.GetFieldDefaultValue != null) - { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_DEFAULT_ATTRIBUTE, order); - } - - serializableFieldSaveFlags[order] = methods with { GetFieldDefaultValue = (IMethodSymbol)symbol }; - } + 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; @@ -220,23 +171,67 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a markDirtyMethod = null; } - // SerializableFieldChanged methods, keyed by order. - var serializableFieldChangedMethods = new Dictionary(); - foreach (var (symbol, attrData) in changedMethods) + // Linkage: [SaveFlag] and [DeserializeTimer] on the serializable members themselves. + // The named methods must exist with the expected shapes. + var serializableFieldSaveFlags = new SortedDictionary(); + var timerLinks = new Dictionary(); + + foreach (var (symbol, attributeData) in fields.Concat(properties)) { - var order = (int)attrData.ConstructorArguments[0].Value!; + token.ThrowIfCancellationRequested(); + var order = (int)attributeData.ConstructorArguments[0].Value!; if (order < 0) { - return Fail(DiagnosticDescriptors.SG3006, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, symbol.Name); + continue; } - if (serializableFieldChangedMethods.ContainsKey(order)) + var memberType = (symbol as IFieldSymbol)?.Type ?? ((IPropertySymbol)symbol).Type; + + foreach (var attr in symbol.GetAttributes()) { - return Fail(DiagnosticDescriptors.SG3003, SymbolMetadata.SERIALIZABLE_FIELD_CHANGED_ATTRIBUTE, order); - } + if (attr.IsSaveFlag(compilation)) + { + 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, memberType)); + if (defaultMethod == null) + { + return Fail(DiagnosticDescriptors.SG3015, defaultName, "SaveFlag", $"{memberType} Method()"); + } + } + + serializableFieldSaveFlags[order] = new SerializableFieldSaveFlagMethods + { + DetermineFieldShouldSerialize = shouldMethod, + GetFieldDefaultValue = defaultMethod + }; + } + 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) + ); - serializableFieldChangedMethods[order] = (IMethodSymbol)symbol; + 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,19 +355,51 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a // Readonly fields cannot have setters - force to null var effectiveSetterAccessor = fieldSymbol.IsReadOnly ? (Accessibility?)null : setterAccessor; - serializableFieldChangedMethods.TryGetValue(order, out var fieldChangedMethod); + // 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) + { + if (effectiveSetterAccessor == null) + { + return Fail(DiagnosticDescriptors.SG3018, "fieldChanged", fieldSymbol.Name); + } - if (fieldChangedMethod != null) + 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)" + ); + } + } + + IMethodSymbol? allowFieldChangeMethod = null; + var allowFieldChangeName = attrCtorArgs.Length > 5 ? attrCtorArgs[5].Value as string : null; + if (allowFieldChangeName != 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 (effectiveSetterAccessor == null) + { + return Fail(DiagnosticDescriptors.SG3018, "allowFieldChange", fieldSymbol.Name); + } + + allowFieldChangeMethod = classSymbol.FindLinkedMethod( + allowFieldChangeName, + m => IsAllowChangeShape(m, fieldSymbol.Type) + ); - if (!isValidSignature) + if (allowFieldChangeMethod == null) { - return Fail(DiagnosticDescriptors.SG3010, fieldChangedMethod.Name, fieldType.ToDisplayString()); + return Fail( + DiagnosticDescriptors.SG3015, allowFieldChangeName, "SerializableField allowFieldChange", + $"bool Method(ref {fieldSymbol.Type} value)" + ); } } @@ -419,6 +446,7 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a fieldSymbol.Type.HasInequalityOperator(), invalidateProperties, fieldChangedMethod?.Name, + allowFieldChangeMethod?.Name, attributeLines.ToEquatableArray(), dsIsArray, dsIsDictionary, @@ -471,7 +499,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 +508,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 +594,6 @@ SerializationModelResult Fail(DiagnosticDescriptor descriptor, params object[] a location ); - return new SerializationModelResult(model, EquatableArray.Empty); + return new SerializationModelResult(model, System.Array.Empty().ToEquatableArray()); } } 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/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..70651b0 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,11 +34,8 @@ 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 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 DESERIALIZE_TIMER_ATTRIBUTE = "ModernUO.Serialization.DeserializeTimerAttribute"; + public const string SAVE_FLAG_ATTRIBUTE = "ModernUO.Serialization.SaveFlagAttribute"; public const string SERIALIZED_PROPERTY_ATTR_ATTRIBUTE = "ModernUO.Serialization.SerializedPropertyAttrAttribute`1"; public const string SORTED_SET_COMPARER_ATTRIBUTE = "ModernUO.Serialization.SortedSetComparerAttribute"; @@ -90,8 +88,11 @@ 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 static bool IsTimer(this ITypeSymbol symbol, Compilation compilation) => @@ -317,38 +318,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) @@ -497,26 +485,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/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..234ce28 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,84 @@ 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. Every linkage is +declared on the serializable field itself, naming its companion methods with `nameof()`: + +```cs + // 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; + + private bool ShouldSerializeCharges() => _charges != 8; + + private int ChargesDefaultValue() => 8; + + // 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 +`[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]`: + +```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)]` and `[SerializableFieldDefault(order)]` with + `[SaveFlag(nameof(ShouldSerializeMethod), nameof(DefaultValueMethod))]` on the field, and + `[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 + 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.