ModernUO/dev-docs/claude-skills/modernuo-serialization.md
Kamron Batman 1391c563fe
chore: Adds AI instructions and SKILLs for ModernUO codebase (#2347)
Summary

  - Adds CLAUDE.md at repo root with 14 terse code audit rules (always loaded, low token cost)
  - Adds pointer files for other AI tools: AGENTS.md (Codex), GEMINI.md, .github/COPILOT-INSTRUCTIONS.md (Copilot), .cursorrules (Cursor) — all redirect to CLAUDE.md as single source of truth
  - Gitignores /.claude so personal AI config isn't distributed
  - Moves Claude skills to dev-docs/claude-skills/ (opt-in, not auto-loaded)
  - Adds 14 dev-docs covering codebase conventions

  Code Audit Rules (in CLAUDE.md)

  1. LINQ tiered rules (Tier 1 free, Tier 2 warm, Tier 3 forbidden)
  2. No Console.WriteLine — use LogFactory.GetLogger()
  3. No concurrency primitives in game code
  4. No World.Mobiles/World.Items iteration
  5. Clean up refs in OnDelete()/OnAfterDelete()
  6. Cancel timers in OnDelete()/OnAfterDelete()
  7. STArrayPool<T>.Shared not ArrayPool<T>.Shared
  8. PooledRefList<T> not new List<T>() on hot paths
  9. Serialization: partial class, [Constructible], no serialized TimerExecutionToken
  10. No Task.Run/new Thread() in game code
  11. Never assume era — ask which expansion
  12. _camelCase fields, PascalCase properties/methods
  13. No empty gumps — use DisplayTo() pattern
  14. PropertyList string literals must be {} holes, cliloc-as-argument uses :#
2026-03-01 11:42:19 -08:00

7.6 KiB

name description
modernuo-serialization 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)
[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
[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
[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).

[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.

[AfterDeserialization]
private void AfterDeserialization()
{
    Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}

[DeserializeTimerField(fieldIndex)]

Custom timer deserialization. Timer is saved as remaining TimeSpan.

[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.

[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeMaxItems() => _maxItems != -1;

[SerializableFieldDefault(0)]
private int MaxItemsDefaultValue() => -1;

[TypeAlias(aliases)]

Maps old type names for backward-compatible deserialization.

[TypeAlias("Server.Mobiles.Bear")]
[SerializationGenerator(0, false)]
public partial class BlackBear : BaseCreature { }

Patterns

Minimal Item (Version 0, No Custom Fields)

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

[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

[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();
        }
    }
}

Anti-Patterns

  • Missing partial: [SerializationGenerator] requires partial class
  • Serializing timers: TimerExecutionToken cannot be serialized
  • 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