docs: Updates CLAUDE dev-docs/skills for serialization (#2372)
This commit is contained in:
parent
ff10811d3d
commit
af35c25ca2
15 changed files with 342 additions and 79 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>()` |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue