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 :#
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
- Always use
partialclass when applying[SerializationGenerator] - Always add
[Constructible]on parameterless constructors for Items/Mobiles - Never serialize
TimerExecutionToken-- restore timers in[AfterDeserialization] - Call
this.MarkDirty()in custom property setters that modify serialized state - Use
using ModernUO.Serialization;for serialization attributes - Field order matters --
[SerializableField(N)]index determines serialization order - 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: Usefalsefor Items/Mobiles (defaulttruefor 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 publicsaveIf: 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 orderuseField: 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]requirespartial class - Serializing timers:
TimerExecutionTokencannot be serialized - Missing
MarkDirty(): Custom property setters must callthis.MarkDirty() - Wrong field prefix: Use
_camelCase, notm_camelCasefor new fields - Forgetting
[Constructible]: Items/Mobiles need this for[addcommand
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 documentationdev-docs/claude-skills/modernuo-timers.md- Timer token patternsdev-docs/claude-skills/modernuo-content-patterns.md- Item/Mobile templatesdev-docs/claude-skills/modernuo-property-lists.md- [InvalidateProperties] usage