docs: Updates CLAUDE dev-docs/skills for serialization (#2372)

This commit is contained in:
Kamron Batman 2026-03-15 01:05:03 -07:00 committed by GitHub
parent ff10811d3d
commit af35c25ca2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 342 additions and 79 deletions

View file

@ -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

View file

@ -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>()` |

View file

@ -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

View file

@ -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]

View file

@ -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`

View file

@ -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