ModernUO/dev-docs/claude-skills/modernuo-serialization.md
Kamron Batman 4f9bc1d9f6
feat: Adds AI skills to migrate from RunUO (#2366)
## Summary

Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO.

- **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines)
- **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas
- **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe
- **Updated `CLAUDE.md`** — added migration skill lookup table

### Key migration patterns covered
- Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]`
- `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods
- `Timer` subclasses → `TimerExecutionToken` fire-and-forget
- `Gump` → `StaticGump<T>`/`DynamicGump` with builders
- `EventSink.WorldSave` → `GenericPersistence`
- `ObjectPropertyList` → `IPropertyList` with string hole rules
- Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
2026-03-13 00:33:45 -07:00

295 lines
9.5 KiB
Markdown

---
name: modernuo-serialization
description: >
Trigger when creating or modifying classes inheriting Item, Mobile, BaseCreature, or any type with [SerializationGenerator]. When adding serialized fields. When discussing migration or version bumps.
---
# ModernUO Serialization System
## When This Activates
- Creating/modifying classes that inherit `Item`, `Mobile`, `BaseCreature`, or any serializable type
- Adding `[SerializableField]` or `[SerializableProperty]` attributes
- Bumping serialization versions
- Working with migration schemas
- Discussing save/load behavior
## Key Rules
1. **Always use `partial` class** when applying `[SerializationGenerator]`
2. **Always add `[Constructible]`** on parameterless constructors for Items/Mobiles
3. **Never serialize `TimerExecutionToken`** -- restore timers in `[AfterDeserialization]`
4. **Call `this.MarkDirty()`** in custom property setters that modify serialized state
5. **Use `using ModernUO.Serialization;`** for serialization attributes
6. **Field order matters** -- `[SerializableField(N)]` index determines serialization order
7. **Increment version** when adding, removing, or reordering fields
## Core Attributes
### [SerializationGenerator(version, encodedVersion)]
Applied to class. Generates Serialize/Deserialize methods.
- `version`: Current serialization version (0+)
- `encodedVersion`: Use `false` for Items/Mobiles (default `true` for other types)
```csharp
[SerializationGenerator(0, false)]
public partial class MyItem : Item { }
```
### [SerializableField(index, setter, saveIf)]
Applied to `_camelCase` private fields. Generates `PascalCase` property.
- `index`: Serialization order (0+)
- `setter`: Access level -- `"private"`, `"internal"`, or omit for public
- `saveIf`: Condition method name for conditional serialization
```csharp
[SerializableField(0)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
// Generates: public int Charges { get; set; }
```
### [SerializableProperty(index, useField)]
Applied to properties with custom get/set logic.
- `index`: Serialization order
- `useField`: Backing field name if auto-detection fails
```csharp
[SerializableProperty(0)]
[CommandProperty(AccessLevel.GameMaster)]
public int MaxItems
{
get => _maxItems == -1 ? DefaultMaxItems : _maxItems;
set
{
_maxItems = value;
InvalidateProperties();
this.MarkDirty();
}
}
```
### [InvalidateProperties]
On serialized fields -- auto-calls `InvalidateProperties()` when field changes (refreshes client tooltip).
```csharp
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private bool _balanced;
```
### [SerializedCommandProperty(accessLevel)]
Exposes field to `[Props` gump for in-game editing.
### [EncodedInt]
Variable-length int encoding (saves space for small values).
### [DeltaDateTime]
Stores DateTime as offset from current time (handles server restarts).
### [InternString]
Interns strings to reduce memory for repeated values.
### [Tidy]
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.
```csharp
[AfterDeserialization]
private void AfterDeserialization()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}
```
### [DeserializeTimerField(fieldIndex)]
Custom timer deserialization. Timer is saved as remaining TimeSpan.
```csharp
[SerializableField(0, setter: "private")]
private Timer _evaluateTimer;
[DeserializeTimerField(0)]
private void DeserializeEvaluateTimer(TimeSpan delay)
{
_evaluateTimer = Timer.DelayCall(delay, EvaluationInterval, Evaluate);
}
```
### [SerializableFieldSaveFlag(fieldIndex)] / [SerializableFieldDefault(fieldIndex)]
Conditional serialization -- skip fields with default values.
```csharp
[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeMaxItems() => _maxItems != -1;
[SerializableFieldDefault(0)]
private int MaxItemsDefaultValue() => -1;
```
### [TypeAlias(aliases)]
Maps old type names for backward-compatible deserialization.
```csharp
[TypeAlias("Server.Mobiles.Bear")]
[SerializationGenerator(0, false)]
public partial class BlackBear : BaseCreature { }
```
## Patterns
### Minimal Item (Version 0, No Custom Fields)
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class MyItem : Item
{
[Constructible]
public MyItem() : base(0x1234)
{
Weight = 1.0;
}
public override string DefaultName => "a my item";
}
```
### Item with Fields
```csharp
[SerializationGenerator(0, false)]
public partial class ChargedItem : Item
{
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private Mobile _owner;
private TimerExecutionToken _timerToken; // NOT serialized
[Constructible]
public ChargedItem() : base(0x1234) => _charges = 10;
[AfterDeserialization]
private void AfterDeserialization()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}
public override void OnAfterDelete()
{
_timerToken.Cancel();
base.OnAfterDelete();
}
}
```
### Item with Custom Properties
```csharp
[SerializationGenerator(2, false)]
public partial class BagOfSending : Item
{
[SerializableProperty(0)]
[CommandProperty(AccessLevel.GameMaster)]
public BagOfSendingHue BagOfSendingHue
{
get => _bagOfSendingHue;
set
{
_bagOfSendingHue = value;
Hue = value switch
{
BagOfSendingHue.Yellow => 0x8A5,
BagOfSendingHue.Blue => 0x8AD,
BagOfSendingHue.Red => 0x89B,
_ => Hue
};
this.MarkDirty();
}
}
}
```
## Custom Serialize/Deserialize (Purity Rules)
When writing custom `Serialize(IGenericWriter)` or `Deserialize(IGenericReader)` methods (e.g. for `GenericPersistence` subclasses), the following rules apply:
### Serialize() MUST remain pure
`Serialize()` is called from **background serialization threads** during world saves (see `SerializationThreadWorker`). Multiple entities are serialized in parallel across threads. This means `Serialize()` must NOT:
- **Create or destroy Items/Mobiles** -- mutates shared world state
- **Move, equip, or unequip Items/Mobiles** -- mutates shared world state
- **Start or stop timers** (`Timer.StartTimer`, `Timer.DelayCall`, `_token.Cancel()`) -- timers are NOT thread-safe
- **Send packets or modify NetState** -- networking is game-thread-only
- **Access or modify other entities' mutable state** -- data race
- **Call `Delete()`** on anything -- triggers deletion cascades on wrong thread
`Serialize()` should ONLY read fields and write them to the `IGenericWriter`. Treat it as a read-only snapshot.
```csharp
// CORRECT -- pure reads and writes only
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_records.Count);
foreach (var (key, value) in _records)
{
writer.Write(key);
writer.Write(value);
}
}
// WRONG -- side effects in Serialize
public override void Serialize(IGenericWriter writer)
{
CleanupExpiredEntries(); // BAD: mutates state
Timer.StartTimer(Recheck); // BAD: not thread-safe
writer.Write(_data);
}
```
### 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.
## Anti-Patterns
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
- **Serializing timers**: `TimerExecutionToken` cannot be serialized
- **Side effects in `Serialize()`**: Serialize runs on background threads -- must be pure (no creating/destroying entities, no timer start/stop, no packets)
- **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
## Real Examples
- Simple creature: `Projects/UOContent/Mobiles/Animals/Bears/BlackBear.cs`
- Serialized fields + timer: `Projects/UOContent/Items/Weapons/Ranged/BaseRanged.cs`
- Custom properties: `Projects/UOContent/Items/Special/Solen Items/BagOfSending.cs`
- Complex with AfterDeserialization: `Projects/UOContent/Accounting/Account.cs`
- Timer deserialization: `Projects/UOContent/Items/Aquarium/Aquarium.cs`
- Tidy + DeltaDateTime: `Projects/UOContent/Engines/CannedEvil/ChampionSpawn.cs`
- Conditional serialization: `Projects/Server/Items/Container.cs`
## Version Migration
Migration schemas are JSON files in `Projects/Server/Migrations/` and `Projects/UOContent/Migrations/`:
- Format: `TypeName.vN.json`
- Generated automatically by the serialization generator
- Used for reading old save formats
External reference: https://github.com/modernuo/SerializationGenerator
## See Also
- `dev-docs/serialization.md` - Complete serialization documentation
- `dev-docs/claude-skills/modernuo-timers.md` - Timer token patterns
- `dev-docs/claude-skills/modernuo-content-patterns.md` - Item/Mobile templates
- `dev-docs/claude-skills/modernuo-property-lists.md` - [InvalidateProperties] usage