docs: update serialization docs and skills for generator v4

Replaces order-based linkage ([SerializableFieldSaveFlag]/[SerializableFieldDefault])
with field-side [SaveFlag(nameof(...))], [TimerDrift]/[DeserializeTimerField] with
[DeserializeTimer(nameof(Method), wallClock)], and documents the [SerializableField]
setter hooks (allowFieldChange/fieldChanged), the anchored-time semantics for
drifting timers, [AnchoredDateTime], and the timer MigrateFrom pattern. Narrows
[SerializableProperty] guidance to custom getters. Also fixes the documented
[SerializableField] signature (the saveIf parameter never existed) and refreshes
real-code examples that were converted in the v4 migration PRs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Kamron Batman 2026-08-22 18:26:56 -07:00
parent b042edcf0b
commit 76bbcd88c4
No known key found for this signature in database
GPG key ID: 7D81DF26D9A5D94A
7 changed files with 219 additions and 63 deletions

View file

@ -457,16 +457,24 @@ set
```
Without this, changes won't be saved.
Most RunUO custom setters only clamp the value or run side effects after assignment. Those
convert to a plain `[SerializableField]` with the `allowFieldChange`/`fieldChanged` hooks,
which handle the equality check and `MarkDirty()` for you -- reserve `[SerializableProperty]`
for custom getters (see `dev-docs/serialization.md`).
### 3. Field Ordering
The `[SerializableField(N)]` index determines serialization order. Choose a logical order and don't change it after the first save — or increment the version.
### 4. Conditional Serialization
Use `[SerializableFieldSaveFlag]` and `[SerializableFieldDefault]` to skip default values:
Use `[SaveFlag]` on the serializable field to skip default values (the second method is
optional -- omit it and the field keeps its default at load):
```csharp
[SerializableFieldSaveFlag(0)]
[SerializableField(0)]
[SaveFlag(nameof(ShouldSerializeMaxItems), nameof(MaxItemsDefaultValue))]
private int _maxItems;
private bool ShouldSerializeMaxItems() => _maxItems != -1;
[SerializableFieldDefault(0)]
private int MaxItemsDefaultValue() => -1;
```
@ -479,12 +487,15 @@ private List<Mobile> _followers;
```
### 6. DateTime Fields
Use `[DeltaDateTime]` to survive server restarts:
Use `[AnchoredDateTime]` to survive server restarts -- the value is shifted by downtime at
load, so the remaining time is preserved and idle saves stay byte-stable:
```csharp
[DeltaDateTime]
[AnchoredDateTime]
[SerializableField(0)]
private DateTime _expireTime;
```
(`[DeltaDateTime]` is the legacy equivalent; it rewrites bytes on every save. Converting an
existing field between the two changes the wire format and requires a version bump.)
### 7. Keeping Manual Serialization (Rare)
Some edge cases still need manual serialization. If a type has complex conditional logic that can't be expressed with attributes, you can implement `ISerializable` manually. But this is rare — try attributes first.

View file

@ -305,7 +305,7 @@ In RunUO, timers are commonly started in `Deserialize()`. In ModernUO, use `[Aft
`_token.Cancel()` can be called on a default token, a stopped token, or an already-cancelled token. No null checks needed.
### 4. Timer.DelayCall Still Exists
`Timer.DelayCall()` is still available and returns a `Timer` object. Use it when you need the `Timer` reference (e.g., for `[DeserializeTimerField]`) or state-carrying overloads.
`Timer.DelayCall()` is still available and returns a `Timer` object. Use it when you need the `Timer` reference (e.g., for a serialized timer field with `[DeserializeTimer]`) or state-carrying overloads.
### 5. Custom Timer Classes Are Still Possible
For complex timer logic (e.g., `Corpse.DecayTimer`), you can still subclass `Timer` with `OnTick()`. But prefer the fire-and-forget pattern for simple cases.