docs: Updates CLAUDE dev-docs/skills for serialization (#2372)

This commit is contained in:
Kamron Batman 2026-03-15 01:05:03 -07:00 committed by GitHub
parent ff10811d3d
commit af35c25ca2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 342 additions and 79 deletions

View file

@ -18,12 +18,14 @@ Apply these when writing or reviewing `.cs` files under `Projects/`.
6. **Cancel timers in `OnDelete()`/`OnAfterDelete()`** — call `_token.Cancel()` or `_timer?.Stop()`
7. **`STArrayPool<T>.Shared`** not `ArrayPool<T>.Shared` — single-threaded optimized, no locks
8. **`PooledRefList<T>`** not `new List<T>()` on hot paths — zero GC pressure, stack-allocated ref struct
9. **Serialization** — class must be `partial`, constructor needs `[Constructible]`, `TimerExecutionToken` must NOT have `[SerializableField]`
9. **Serialization** — class must be `partial`, constructor needs `[Constructible]`, `TimerExecutionToken` must NOT have `[SerializableField]`. New classes: use `[SerializationGenerator(version)]` (omit `encoded`). When bumping versions, add `MigrateFrom(VXContent)` (X = previous version). Never modify `Deserialize(reader, version)` for version bumps — that method is only for pre-codegen legacy saves. When migrating from pre-codegen Serialize/Deserialize: pass `false` if old code used `reader.ReadInt()`, bump version +1, and keep old logic as `private void Deserialize(IGenericReader reader, int version)``dev-docs/runuo-migration-docs/02-serialization.md`
10. **No `Task.Run`/`new Thread()`** in game code — game logic is single-threaded event loop
11. **Never assume era** — if code uses `Core.AOS`/`Core.SE`/etc., ask which expansion to target
12. **Naming**`_camelCase` private fields, `PascalCase` properties/methods/classes; don't flag legacy `m_` but use `_` for new code
13. **No empty gumps** — every gump must produce visual elements. An empty gump leaks on client+server (no way to close it). Use static `DisplayTo()` to validate before constructing → `dev-docs/gump-system.md`
14. **PropertyList string literals must be holes**`$"{"Map"}\t{value}"` not `$"Map\t{value}"`. The handler treats bare text as delimiters, `{}` holes as arguments. Only `\t` should be a bare literal → `dev-docs/property-lists.md`
15. **Braces required on all control flow**`if`, `else`, `for`, `foreach`, `while`, `do`, `switch` must always have braces, even for single-line bodies → `dev-docs/code-standards.md`
16. **Prefer switch expressions and switch-when** — use switch expressions for value mapping and switch-when for pattern matching where they improve readability. Exception: skip if unreadable or cold path → `dev-docs/code-standards.md`
## Dev-Docs Reference

View file

@ -14,7 +14,7 @@ description: >
## Item Conversion Checklist
1. [ ] Foundation: file-scoped namespace, `using ModernUO.Serialization;`
2. [ ] Class: add `[SerializationGenerator(0, false)]`, add `partial`
2. [ ] Class: add `[SerializationGenerator(N, false)]` (N = old version + 1, `false` if old `Deserialize` used `ReadInt()`), add `partial`
3. [ ] Fields: `m_X` -> `[SerializableField(N)] _x` with `[SerializedCommandProperty]`
4. [ ] Add `[InvalidateProperties]` where RunUO setter called `InvalidateProperties()`
5. [ ] Delete: Serial constructor, Serialize, Deserialize

View file

@ -15,25 +15,27 @@ description: >
## Conversion Steps
1. Add `using ModernUO.Serialization;`
2. Add `[SerializationGenerator(0, false)]` to class (version 0, false for Item/Mobile)
3. Add `partial` to class declaration
4. Convert each serialized field: `private int m_X` -> `[SerializableField(N)] private int _x`
5. Add `[SerializedCommandProperty(AccessLevel.X)]` if RunUO had `[CommandProperty]`
6. Add `[InvalidateProperties]` if setter called `InvalidateProperties()`
7. DELETE the `Serial` constructor
8. DELETE `Serialize()` and `Deserialize()` overrides
9. Change `[Constructable]` to `[Constructible]`
10. Timer fields: leave unserialized, add `[AfterDeserialization]` method
2. Read the old `Serialize()` to find the version number it writes. Bump it by 1 for `[SerializationGenerator]`
3. If old `Deserialize()` used `reader.ReadInt()` (not `ReadEncodedInt()`), pass `false` as second parameter: `[SerializationGenerator(N, false)]`
4. Add `partial` to class declaration
5. Convert each serialized field: `private int m_X` -> `[SerializableField(N)] private int _x`
6. Add `[SerializedCommandProperty(AccessLevel.X)]` if RunUO had `[CommandProperty]`
7. Add `[InvalidateProperties]` if setter called `InvalidateProperties()`
8. DELETE the `Serial` constructor
9. DELETE the `Serialize()` override
10. Convert `Deserialize()` to `private void Deserialize(IGenericReader reader, int version)` to handle pre-codegen saves (remove `override`, `base.Deserialize()`, and version read line). Delete entirely if no existing saves.
11. Change `[Constructable]` to `[Constructible]`
12. Timer fields: leave unserialized, add `[AfterDeserialization]` method
## Quick Mapping
| RunUO | ModernUO |
|---|---|
| `public class Foo : Item` | `[SerializationGenerator(0, false)] public partial class Foo : Item` |
| `public class Foo : Item` | `[SerializationGenerator(N, false)] public partial class Foo : Item` (N = old version + 1) |
| `private int m_X` + manual Serialize | `[SerializableField(0)] private int _x` |
| `[CommandProperty(GM)]` on property | `[SerializedCommandProperty(GM)]` on field |
| `Foo(Serial serial) : base(serial)` | DELETE |
| `Serialize(GenericWriter)` | DELETE -- auto-generated |
| `Deserialize(GenericReader)` | DELETE -- auto-generated |
| `Deserialize(GenericReader)` | Convert to `private void Deserialize(IGenericReader reader, int version)` for old saves |
| Custom setter with InvalidateProperties() | `[InvalidateProperties]` attribute |
| Custom setter logic | `[SerializableProperty(N)]` with `this.MarkDirty()` |
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` |

View file

@ -127,10 +127,46 @@ public override void OnAfterDelete()
**Why**: The handler treats bare text as delimiters and `{}` contents as arguments. The property list system is used beyond the game client (e.g., web rendering) which must distinguish arguments from delimiters. Only `\t` should be a bare literal.
**Also**: If you don't know the text for a cliloc number, see `Projects/Server/Localization/Localization.cs` `LoadClilocs()` to learn the binary format, and ask the user where their `cliloc.enu` file is.
### 15. Braces Required on All Control Flow
**Check**: ALL `if`, `else`, `for`, `foreach`, `while`, `do`, `switch` statements must have braces, even for single-line bodies.
**Bad**:
```csharp
if (condition)
DoSomething();
```
**Good**:
```csharp
if (condition)
{
DoSomething();
}
```
**Why**: Reduces merge conflicts and diff sizes.
### 16. Prefer Switch Expressions and Switch-When Patterns
**Check**: Where a chain of `if`/`else if` maps inputs to outputs, prefer a switch expression. Where pattern matching with guards improves clarity, prefer `switch`-`when`.
**Bad**:
```csharp
if (type == GemType.StarSapphire) return "star sapphire";
else if (type == GemType.Emerald) return "emerald";
else return "gem";
```
**Good**:
```csharp
return type switch
{
GemType.StarSapphire => "star sapphire",
GemType.Emerald => "emerald",
_ => "gem"
};
```
**Why**: Switch expressions enable JIT/PGO optimization and improve readability.
**Exception**: Skip if the switch would be unreadable or the code is on a cold path.
## Severity Levels
- **ERROR**: Rules 3, 9, 10, 13 (will cause bugs, build failures, or client-side leaks)
- **WARNING**: Rules 1 (Tier 3 LINQ), 2, 4, 5, 6, 7, 8, 12, 14 (performance/convention issues)
- **INFO**: Rule 1 (Tier 2 LINQ on warm paths — note it but don't flag as violation)
- **WARNING**: Rules 1 (Tier 3 LINQ), 2, 4, 5, 6, 7, 8, 12, 14, 15 (performance/convention issues)
- **INFO**: Rules 1 (Tier 2 LINQ on warm paths — note it but don't flag as violation), 16 (switch patterns — suggest but don't flag)
- **ASK**: Rule 11 (need user input)
## How to Report

View file

@ -31,7 +31,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MyItem : Item
{
[Constructible]
@ -72,7 +72,7 @@ using Server.Items;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MyCreature : BaseCreature
{
[Constructible]

View file

@ -25,14 +25,19 @@ description: >
## Core Attributes
### [SerializationGenerator(version, encodedVersion)]
### [SerializationGenerator(version, encoded)]
Applied to class. Generates Serialize/Deserialize methods.
- `version`: Current serialization version (0+)
- `encodedVersion`: Use `false` for Items/Mobiles (default `true` for other types)
- `encoded`: Omit for new classes. When migrating from pre-codegen Serialize/Deserialize, pass `false` if old code used `reader.ReadInt()` (not `ReadEncodedInt()`)
```csharp
[SerializationGenerator(0, false)]
// New class — omit encoded
[SerializationGenerator(0)]
public partial class MyItem : Item { }
// Migration from pre-codegen — old version was 2, used ReadInt()
[SerializationGenerator(3, false)]
public partial class MigratedItem : Item { }
```
### [SerializableField(index, setter, saveIf)]
@ -96,15 +101,31 @@ Auto-removes null/deleted entries from collections after deserialization.
### [CanBeNull]
Marks field as nullable during deserialization.
### [AfterDeserialization]
Method called after all fields are deserialized. Use for initialization, timer restoration, relationship setup.
### [AfterDeserialization(synchronous)]
Method called after fields are deserialized. The `synchronous` parameter controls timing:
- `true` (default): runs immediately after this entity's deserialization
- `false`: runs after ALL entities in the world are deserialized
Use `true` (default) for: restarting timers, setting up derived values from own fields.
Use `false` for: logic that calls `Delete()`, depends on other entities, or affects game state.
```csharp
// Sync (default) — only touches own fields
[AfterDeserialization]
private void AfterDeserialization()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}
// Deferred — calls Delete() which affects game state
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (_expireTimer == null)
{
Delete();
}
}
```
### [DeserializeTimerField(fieldIndex)]
@ -137,7 +158,7 @@ Maps old type names for backward-compatible deserialization.
```csharp
[TypeAlias("Server.Mobiles.Bear")]
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class BlackBear : BaseCreature { }
```
@ -149,7 +170,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MyItem : Item
{
[Constructible]
@ -164,7 +185,7 @@ public partial class MyItem : Item
### Item with Fields
```csharp
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class ChargedItem : Item
{
[SerializableField(0)]
@ -197,7 +218,7 @@ public partial class ChargedItem : Item
### Item with Custom Properties
```csharp
[SerializationGenerator(2, false)]
[SerializationGenerator(2)]
public partial class BagOfSending : Item
{
[SerializableProperty(0)]
@ -262,6 +283,38 @@ public override void Serialize(IGenericWriter writer)
### Deserialize() runs on the game thread
`Deserialize()` runs during world load on the main thread, so it CAN create entities and start timers. However, prefer `[AfterDeserialization]` for timer setup to keep deserialization clean.
## MigrateFrom Pattern
When bumping the `[SerializationGenerator]` version, you **must** add a `MigrateFrom` method:
```csharp
// Version bumped from 0 to 1 (added _quality field)
[SerializationGenerator(1)]
public partial class MagicGem : Item
{
[SerializableField(0)]
private int _charges;
[SerializableField(1)] // New in v1
private GemQuality _quality;
}
// In MagicGem.Migrations.cs:
public partial class MagicGem
{
private void MigrateFrom(V0Content content)
{
_charges = content.Charges;
// _quality defaults to GemQuality.Rough (default enum value)
}
}
```
- Signature: `private void MigrateFrom(VXContent content)` where X is the **previous** version
- `VXContent` is auto-generated with PascalCase properties matching the old fields
- New fields not in the old version get their default values
- Use `.Migrations.cs` partial files for organization
## Anti-Patterns
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
@ -270,6 +323,7 @@ public override void Serialize(IGenericWriter writer)
- **Missing `MarkDirty()`**: Custom property setters must call `this.MarkDirty()`
- **Wrong field prefix**: Use `_camelCase`, not `m_camelCase` for new fields
- **Forgetting `[Constructible]`**: Items/Mobiles need this for `[add` command
- **Modifying `Deserialize(reader, version)` for version bumps**: `Deserialize` exists ONLY for pre-codegen legacy saves. Use `MigrateFrom(VXContent)` for all post-codegen version transitions.
## Real Examples
- Simple creature: `Projects/UOContent/Mobiles/Animals/Bears/BlackBear.cs`

View file

@ -111,7 +111,7 @@ public override void OnAfterDelete()
### Timer Restoration After Deserialization
```csharp
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class DecayingItem : Item
{
private TimerExecutionToken _decayTimer; // NOT serialized

View file

@ -43,6 +43,44 @@ This document defines the coding conventions and standards for ModernUO content
var damage = Utility.RandomMinMax(10, 20);
```
### Brace Style
ALL control flow statements (`if`, `else`, `for`, `foreach`, `while`, `do`, `switch`) **must** have braces, even for single-line bodies. This reduces merge conflicts and diff sizes.
```csharp
// BAD
if (condition)
DoSomething();
// GOOD
if (condition)
{
DoSomething();
}
```
### Switch Patterns
Prefer switch expressions and switch-when pattern matching where they improve readability and enable JIT/PGO optimization. Skip if code becomes unreadable or is on a cold path.
```csharp
// Prefer switch expression for value mapping
private static string GetName(GemType type) => type switch
{
GemType.StarSapphire => "star sapphire",
GemType.Emerald => "emerald",
_ => "gem"
};
// Prefer switch-when for performance (compiler hints to JIT/PGO)
switch (item)
{
case Sword { Quality: >= ItemQuality.Exceptional } when Core.AOS:
{
bonus = 10;
break;
}
}
```
### Legacy Code
Older code uses `m_` prefix for private fields (e.g., `m_Amount`). Do not change existing `m_` fields, but always use `_` prefix for new code.
@ -332,7 +370,7 @@ Available spatial queries (on `Map`):
### Partial Classes
Any class with `[SerializationGenerator]` **must** be declared `partial`:
```csharp
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MyItem : Item // MUST be partial
{
}

View file

@ -22,7 +22,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class SimpleItem : Item
{
[Constructible]
@ -42,7 +42,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MagicLantern : Item
{
[SerializableField(0)]
@ -154,7 +154,7 @@ using Server.Items;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class ForestWolf : BaseCreature
{
[Constructible]

View file

@ -79,7 +79,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(2, false)] // Old version was 1 → bump to 2; false because old saves used ReadInt()
public partial class ChargedGem : Item
{
[SerializableField(0)]
@ -98,6 +98,24 @@ public partial class ChargedGem : Item
Weight = 1.0;
}
// Handles loading saves from BEFORE the SerializationGenerator conversion
private void Deserialize(IGenericReader reader, int version)
{
switch (version)
{
case 1:
{
_owner = reader.ReadEntity<Mobile>();
goto case 0;
}
case 0:
{
_charges = reader.ReadInt();
break;
}
}
}
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
@ -124,7 +142,7 @@ public partial class ChargedGem : Item
| `reader.ReadInt()` | `reader.ReadInt()` | Same for manual cases |
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` | Generic method |
| `reader.ReadItem()` | `reader.ReadEntity<Item>()` | Generic method |
| `writer.Write((int)0)` version | `[SerializationGenerator(0, false)]` | In attribute |
| `writer.Write((int)0)` version | `[SerializationGenerator(N, false)]` | Version bumped +1; `false` because old saves used `ReadInt()` |
## Step-by-Step Conversion
@ -134,16 +152,17 @@ using ModernUO.Serialization;
```
### Step 2: Add Class Attributes and `partial`
Read the old `Serialize()` to find the version number it writes. Bump it by 1 for the `[SerializationGenerator]` first parameter. If the old `Deserialize()` used `reader.ReadInt()` (not `ReadEncodedInt()`), pass `false` as the second parameter.
```csharp
// Change:
public class MyItem : Item
// To:
[SerializationGenerator(0, false)]
// To (old Serialize wrote version 0, old Deserialize used ReadInt()):
[SerializationGenerator(1, false)]
public partial class MyItem : Item
```
The version number should be `0` for a fresh migration (you're defining a new serialization schema). Use `false` as the second argument for Item/Mobile subclasses.
### Step 3: Delete Serial Constructor
Remove `public MyItem(Serial serial) : base(serial) { }` entirely.
@ -171,8 +190,29 @@ Add `[InvalidateProperties]` if the RunUO setter called `InvalidateProperties()`
private int _charges;
```
### Step 5: Delete Serialize and Deserialize Methods
Remove both override methods entirely. The source generator creates them.
### Step 5: Migrate Serialize and Deserialize Methods
Delete the `Serialize()` override entirely — the source generator creates it.
For `Deserialize()`: if there are existing saves to support, convert it to `private void Deserialize(IGenericReader reader, int version)` (remove the `override`, change the signature). This method handles loading saves from before the SerializationGenerator conversion. Remove the `base.Deserialize(reader)` call and the version reading line — the generator handles those.
```csharp
// Old RunUO:
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
m_Charges = reader.ReadInt();
}
// Converted — keeps backward compat with old saves:
private void Deserialize(IGenericReader reader, int version)
{
_charges = reader.ReadInt();
}
```
If there are no existing saves to worry about (fresh world), you can delete `Deserialize()` entirely.
### Step 6: Change [Constructable] to [Constructible]
```csharp
@ -266,7 +306,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(1, false)] // Old version was 0 → bump to 1; false because old saves used ReadInt()
public partial class SimpleGem : Item
{
[Constructible]
@ -276,6 +316,12 @@ public partial class SimpleGem : Item
}
public override string DefaultName => "a simple gem";
// Handles loading saves from BEFORE the SerializationGenerator conversion
private void Deserialize(IGenericReader reader, int version)
{
// Version 0 had no custom fields — nothing to read
}
}
```
@ -337,7 +383,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)] // Version 0 — new schema
[SerializationGenerator(3, false)] // Old version was 2 → bump to 3; false because old saves used ReadInt()
public partial class MagicGem : Item
{
[SerializableField(0)]
@ -360,10 +406,37 @@ public partial class MagicGem : Item
_charges = 10;
_quality = GemQuality.Rough;
}
// Handles loading saves from BEFORE the SerializationGenerator conversion
private void Deserialize(IGenericReader reader, int version)
{
switch (version)
{
case 2:
{
_quality = (GemQuality)reader.ReadInt();
goto case 1;
}
case 1:
{
_owner = reader.ReadEntity<Mobile>();
goto case 0;
}
case 0:
{
_charges = reader.ReadInt();
break;
}
}
}
}
```
**Important**: When migrating RunUO code, the ModernUO version starts at 0 because you're defining a new serialization schema. The old version numbers from RunUO are irrelevant — the source generator doesn't read the old format. The old saves must be re-saved or a migration schema must be created.
**Important**: When migrating RunUO code:
- Read the old `Serialize()` to find the version it writes, then bump it by 1 for the `[SerializationGenerator]` first parameter
- Pass `false` as the second parameter if the old `Deserialize()` used `reader.ReadInt()` (not `ReadEncodedInt()`) — this tells the generator how old saves encoded the version number
- Keep the old deserialization logic as `private void Deserialize(IGenericReader reader, int version)` to handle loading pre-codegen saves
- This `Deserialize` method is called automatically for old saves; the generator handles new saves
## Edge Cases & Gotchas

View file

@ -82,7 +82,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MyItem : Item
{
private TimerExecutionToken _timerToken;

View file

@ -16,7 +16,7 @@ Most RunUO migration work involves converting Item, Mobile, and BaseCreature sub
### 2. Add Serialization Attributes
```csharp
[SerializationGenerator(0, false)] // Version 0, Item subclass
[SerializationGenerator(N, false)] // N = old version + 1; false if old Deserialize used ReadInt()
public partial class MyItem : Item // Add partial
```
@ -223,7 +223,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MagicLantern : Item
{
[SerializableField(0)]
@ -296,7 +296,7 @@ public partial class MagicLantern : Item
**What changed:**
- File-scoped namespace
- `partial class` + `[SerializationGenerator(0, false)]`
- `partial class` + `[SerializationGenerator(0)]` (omit `encoded` parameter)
- `[Constructable]``[Constructible]`
- `m_Charges`/`m_Owner``_charges`/`_owner` with `[SerializableField]`
- Manual properties → auto-generated with `[SerializedCommandProperty]`
@ -396,7 +396,7 @@ using ModernUO.Serialization;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class ForestWolf : BaseCreature
{
[Constructible]
@ -492,7 +492,7 @@ Weapons and armor follow the same item pattern but inherit from specialized base
```csharp
// ModernUO weapon example
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class MySpecialSword : BaseSword
{
[Constructible]
@ -530,7 +530,7 @@ public partial class MySpecialSword : BaseSword
If a class changed namespace or name, use `[TypeAlias]`:
```csharp
[TypeAlias("Server.Items.OldName")]
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class NewName : Item { }
```

View file

@ -171,7 +171,7 @@ using Server.Gumps;
namespace Server.Custom.BountySystem;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class BountyBoard : Item
{
[Constructible]

View file

@ -18,7 +18,7 @@ using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class SimpleItem : Item
{
[Constructible]
@ -33,13 +33,13 @@ public partial class SimpleItem : Item
Key requirements:
1. `using ModernUO.Serialization;` for the attributes
2. `[SerializationGenerator(0, false)]` on the class
2. `[SerializationGenerator(0)]` on the class
3. `partial` class declaration
4. `[Constructible]` on the parameterless constructor
### Item with Serialized Fields
```csharp
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class ChargedGem : Item
{
[SerializableField(0)]
@ -91,7 +91,7 @@ public partial class ChargedGem : Item
## Attribute Reference
### [SerializationGenerator(version, encodedVersion)]
### [SerializationGenerator(version, encoded)]
**Target**: Class declaration
**Required**: Yes, for any serializable type
@ -99,18 +99,24 @@ public partial class ChargedGem : Item
| Parameter | Type | Default | Description |
|---|---|---|---|
| `version` | `int` | Required | Current serialization version number |
| `encodedVersion` | `bool` | `true` | `false` for Item/Mobile subclasses; `true` for standalone serializable types |
| `encoded` | `bool` | `true` | How the version was written in old saves. Only needed when migrating from pre-codegen Serialize/Deserialize — pass `false` if old code used `reader.ReadInt()` |
The version number determines which `Deserialize` overload is called. When you add, remove, or reorder fields, increment the version.
For **new classes**, omit the `encoded` parameter:
```csharp
[SerializationGenerator(3, false)] // Version 3, Item/Mobile subclass
[SerializationGenerator(0)] // New class — omit encoded
public partial class MyItem : Item { }
[SerializationGenerator(0)] // Version 0, standalone type (encodedVersion=true)
public partial class MyData { }
```
When **migrating from RunUO/pre-codegen** classes that used `reader.ReadInt()` for version, pass `false` and bump the version:
```csharp
[SerializationGenerator(3, false)] // Old version was 2 → bump to 3; false because old saves used ReadInt()
public partial class MyItem : Item { }
```
See `dev-docs/runuo-migration-docs/02-serialization.md` for complete migration guidance.
### [SerializableField(index, setter, saveIf)]
**Target**: Private field (`_camelCase`)
@ -245,35 +251,41 @@ private List<Mobile> _followers;
private Mobile _target;
```
### [AfterDeserialization(skipOnDelete)]
### [AfterDeserialization(synchronous)]
**Target**: Parameterless private method
**Effect**: Called after all fields are deserialized.
**Effect**: Called after fields are deserialized. The `synchronous` parameter controls execution timing.
| Parameter | Type | Default | Description |
|---|---|---|---|
| `skipOnDelete` | `bool` | `true` | Skip if entity is being deleted |
| `synchronous` | `bool` | `true` | `true` = runs immediately after this entity's deserialization. `false` = runs after ALL entities in the world are deserialized. |
Common uses:
- Restart timers
- Set up object relationships
- Calculate derived values
- Clean up empty collections
**When to use `true` (default):**
- Restarting timers for this entity
- Setting up derived values from this entity's own fields
- Cleaning up this entity's own collections
**When to use `false` (deferred):**
- Logic that adds or deletes entities (e.g., `Delete()`)
- Logic that depends on other entities being fully loaded (cross-entity references)
- Logic that affects game state
```csharp
// Sync (default) — only touches own fields
[AfterDeserialization]
private void AfterDeserialization()
{
// Restart timers
Timer.StartTimer(TimeSpan.FromMinutes(1), CheckExpiry, out _timerToken);
Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}
// Set up relationships
if (_owner != null)
_owner.OwnedItems.Add(this);
// Clean up
if (_entries?.Count == 0)
_entries = null;
// Deferred — calls Delete() which affects game state
[AfterDeserialization(false)]
private void AfterDeserialization()
{
if (_expireTimer == null)
{
Delete();
}
}
```
@ -321,7 +333,7 @@ private int MaxItemsDefaultValue() => -1;
```csharp
[TypeAlias("Server.Mobiles.Bear")]
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class BlackBear : BaseCreature { }
```
@ -388,6 +400,52 @@ Example: `Server.Accounting.Account.v6.json`
| `SerializableInterfaceMigrationRule` | Objects implementing ISerializable |
| `SerializationMethodSignatureMigrationRule` | Objects with custom Deserialize methods |
### MigrateFrom Pattern
When you bump the `[SerializationGenerator]` version number, you **must** add a `MigrateFrom` method to handle the transition from the previous version. The serialization generator creates a `VXContent` struct (where X is the previous version number) containing PascalCase properties matching the old serialized fields.
```csharp
// Version bumped from 0 to 1 (added _quality field)
[SerializationGenerator(1)]
public partial class MagicGem : Item
{
[SerializableField(0)]
private int _charges;
[SerializableField(1)] // New in v1
private GemQuality _quality;
}
// In MagicGem.Migrations.cs:
public partial class MagicGem
{
private void MigrateFrom(V0Content content)
{
_charges = content.Charges;
// _quality defaults to GemQuality.Rough (default enum value)
}
}
```
**Key rules:**
- The method signature is `private void MigrateFrom(VXContent content)` where X is the **previous** version
- `VXContent` is auto-generated with PascalCase properties matching the old fields
- New fields not present in the old version get their default values
- Organize migrations in a separate `.Migrations.cs` partial file for clarity
### Deserialize vs MigrateFrom — Know the Difference
> **WARNING**: Do NOT modify `Deserialize(IGenericReader reader, int version)` when bumping SerializationGenerator versions.
The two mechanisms serve different purposes:
| Method | When It Runs | Purpose |
|---|---|---|
| `Deserialize(IGenericReader reader, int version)` | Loading saves from **before** the class was converted to the SerializationGenerator | One-time legacy migration only |
| `MigrateFrom(VXContent content)` | Transitioning between SerializationGenerator versions | All post-codegen version bumps |
The `Deserialize` function exists **only** to handle deserialization of entities saved before they used `[SerializationGenerator]`. Once a class uses the generator, all future version transitions must use `MigrateFrom`.
---
## Extension Methods
@ -428,7 +486,7 @@ public enum GemQuality
Flawless
}
[SerializationGenerator(1, false)] // Version 1 (added Quality in v1)
[SerializationGenerator(1)] // Version 1 (added Quality in v1)
public partial class MagicGem : Item
{
[SerializableField(0)]

View file

@ -155,7 +155,7 @@ private void CheckExpiry()
### Pattern 3: Timer Restoration After Deserialization
```csharp
[SerializationGenerator(0, false)]
[SerializationGenerator(0)]
public partial class TimedItem : Item
{
private TimerExecutionToken _timer; // NOT serialized