docs: Updates CLAUDE dev-docs/skills for serialization (#2372)
This commit is contained in:
parent
ff10811d3d
commit
af35c25ca2
15 changed files with 342 additions and 79 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>()` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
{
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ using ModernUO.Serialization;
|
|||
|
||||
namespace Server.Items;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
[SerializationGenerator(0)]
|
||||
public partial class MyItem : Item
|
||||
{
|
||||
private TimerExecutionToken _timerToken;
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ using Server.Gumps;
|
|||
|
||||
namespace Server.Custom.BountySystem;
|
||||
|
||||
[SerializationGenerator(0, false)]
|
||||
[SerializationGenerator(0)]
|
||||
public partial class BountyBoard : Item
|
||||
{
|
||||
[Constructible]
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue