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.