diff --git a/CLAUDE.md b/CLAUDE.md index bfbb83a14..0a7a456e0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.Shared`** not `ArrayPool.Shared` — single-threaded optimized, no locks 8. **`PooledRefList`** not `new List()` 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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md index 0155f2974..8edf70a72 100644 --- a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md @@ -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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md index 4d74bccb0..aa7f8d38c 100644 --- a/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md @@ -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()` | diff --git a/dev-docs/claude-skills/modernuo-code-audit.md b/dev-docs/claude-skills/modernuo-code-audit.md index 2a233a7d2..4c8151d7d 100644 --- a/dev-docs/claude-skills/modernuo-code-audit.md +++ b/dev-docs/claude-skills/modernuo-code-audit.md @@ -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 diff --git a/dev-docs/claude-skills/modernuo-content-patterns.md b/dev-docs/claude-skills/modernuo-content-patterns.md index fa4450bd8..12c9515ef 100644 --- a/dev-docs/claude-skills/modernuo-content-patterns.md +++ b/dev-docs/claude-skills/modernuo-content-patterns.md @@ -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] diff --git a/dev-docs/claude-skills/modernuo-serialization.md b/dev-docs/claude-skills/modernuo-serialization.md index d09068c06..dae1b8620 100644 --- a/dev-docs/claude-skills/modernuo-serialization.md +++ b/dev-docs/claude-skills/modernuo-serialization.md @@ -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` diff --git a/dev-docs/claude-skills/modernuo-timers.md b/dev-docs/claude-skills/modernuo-timers.md index c9160c8d0..2c5c8168e 100644 --- a/dev-docs/claude-skills/modernuo-timers.md +++ b/dev-docs/claude-skills/modernuo-timers.md @@ -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 diff --git a/dev-docs/code-standards.md b/dev-docs/code-standards.md index 40e2e272d..156281400 100644 --- a/dev-docs/code-standards.md +++ b/dev-docs/code-standards.md @@ -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 { } diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md index 0551ef6fb..30bb942f3 100644 --- a/dev-docs/content-patterns.md +++ b/dev-docs/content-patterns.md @@ -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] diff --git a/dev-docs/runuo-migration-docs/02-serialization.md b/dev-docs/runuo-migration-docs/02-serialization.md index be6010bbf..1470792af 100644 --- a/dev-docs/runuo-migration-docs/02-serialization.md +++ b/dev-docs/runuo-migration-docs/02-serialization.md @@ -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(); + 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()` | Generic method | | `reader.ReadItem()` | `reader.ReadEntity()` | 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(); + 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 diff --git a/dev-docs/runuo-migration-docs/03-timers.md b/dev-docs/runuo-migration-docs/03-timers.md index 8331d470b..29f3d1282 100644 --- a/dev-docs/runuo-migration-docs/03-timers.md +++ b/dev-docs/runuo-migration-docs/03-timers.md @@ -82,7 +82,7 @@ using ModernUO.Serialization; namespace Server.Items; -[SerializationGenerator(0, false)] +[SerializationGenerator(0)] public partial class MyItem : Item { private TimerExecutionToken _timerToken; diff --git a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md index f31556194..c5029131c 100644 --- a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md +++ b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md @@ -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 { } ``` diff --git a/dev-docs/runuo-migration-docs/10-systems-engines.md b/dev-docs/runuo-migration-docs/10-systems-engines.md index 329ff1374..44f811dca 100644 --- a/dev-docs/runuo-migration-docs/10-systems-engines.md +++ b/dev-docs/runuo-migration-docs/10-systems-engines.md @@ -171,7 +171,7 @@ using Server.Gumps; namespace Server.Custom.BountySystem; -[SerializationGenerator(0, false)] +[SerializationGenerator(0)] public partial class BountyBoard : Item { [Constructible] diff --git a/dev-docs/serialization.md b/dev-docs/serialization.md index de3b110d4..ecd41cdea 100644 --- a/dev-docs/serialization.md +++ b/dev-docs/serialization.md @@ -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 _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)] diff --git a/dev-docs/timers.md b/dev-docs/timers.md index 849f2bebe..c10f31bd6 100644 --- a/dev-docs/timers.md +++ b/dev-docs/timers.md @@ -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