feat: Adds AI skills to migrate from RunUO (#2366)

## Summary

Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO.

- **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines)
- **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas
- **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe
- **Updated `CLAUDE.md`** — added migration skill lookup table

### Key migration patterns covered
- Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]`
- `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods
- `Timer` subclasses → `TimerExecutionToken` fire-and-forget
- `Gump` → `StaticGump<T>`/`DynamicGump` with builders
- `EventSink.WorldSave` → `GenericPersistence`
- `ObjectPropertyList` → `IPropertyList` with string hole rules
- Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
This commit is contained in:
Kamron Batman 2026-03-13 00:33:45 -07:00 • committed by GitHub
parent 31b22d4773
commit 4f9bc1d9f6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 4216 additions and 7 deletions

View file

@ -0,0 +1,39 @@
---
name: migrate-commands-events
description: >
Trigger: when converting RunUO command registration, EventSink handlers, or event delegate patterns.
Covers: event name changes, delegate removal, Configure vs Initialize.
---
# RunUO -> ModernUO Commands & Events Migration
## When This Activates
- Converting `EventSink` subscriptions
- Converting event handler signatures
- Moving from `Initialize()` to `Configure()`
- Converting WorldSave/WorldLoad events to GenericPersistence
## Conversion Steps
1. Change `Initialize()` to `Configure()` for event registration
2. Remove delegate constructors: `new LoginEventHandler(OnLogin)` -> `OnLogin`
3. Rename events: `Login` -> `Connected`, `Logout` -> `Disconnected`
4. Update signatures: `OnLogin(LoginEventArgs e)` -> `OnConnected(Mobile m)`
5. WorldSave persistence -> convert to `GenericPersistence` (see migrate-persistence skill)
## Event Mapping
| RunUO | ModernUO |
|---|---|
| `EventSink.Login` | `EventSink.Connected` (Action<Mobile>) |
| `EventSink.Logout` | `EventSink.Disconnected` (Action<Mobile>) |
| `EventSink.WorldSave` | `EventSink.WorldSave` (Action -- no args) |
| `EventSink.Crashed` | `EventSink.ServerCrashed` |
| `new XXXEventHandler(method)` | `method` (direct reference) |
## Commands (Minimal Changes)
Commands use the same `CommandSystem.Register()` API. Main change: use `Configure()` for registration.
## See Also
- `dev-docs/runuo-migration-docs/07-commands-events.md` -- detailed migration reference
- `dev-docs/events.md` -- complete ModernUO event system
- `dev-docs/claude-skills/modernuo-events.md` -- ModernUO events skill
- `dev-docs/claude-skills/modernuo-commands-targeting.md` -- ModernUO commands skill

View file

@ -0,0 +1,50 @@
---
name: migrate-foundation
description: >
Trigger: when migrating ANY RunUO code to ModernUO. Always load this skill first.
Covers: namespace changes, naming conventions, attribute renames, logging, threading, performance.
---
# RunUO -> ModernUO Foundation Migration
## When This Activates
- Converting ANY RunUO 2.7 script to ModernUO
- Always apply these changes FIRST before system-specific migration
## Universal Changes Checklist
1. File-scoped namespace: `namespace X { ... }` -> `namespace X;`
2. `using ModernUO.Serialization;` -- add for any serializable type
3. Rename fields: `m_FieldName` -> `_fieldName`
4. `[Constructable]` -> `[Constructible]`
5. `Console.WriteLine` -> `LogFactory.GetLogger(typeof(X))` -> `logger.Information(...)`
6. `DateTime.UtcNow` -> `Core.Now`
7. `World.Mobiles`/`World.Items` iteration -> spatial queries (`map.GetMobilesInRange<T>()`)
8. Remove `lock`, `volatile`, `ConcurrentDictionary`, `Mutex` -- server is single-threaded
9. Remove `Task.Run`, `new Thread` -- use `Timer.StartTimer()` instead
10. `ArrayPool<T>.Shared` -> `STArrayPool<T>.Shared`
11. `new List<T>()` on hot paths -> `PooledRefList<T>.Create()`
12. Modernize property syntax: `{ get { return x; } }` -> `{ get => x; }`
13. Delete `MyType(Serial serial) : base(serial)` constructor -- auto-generated
14. `Name = "text"` in constructor -> `public override string DefaultName => "text";`
## Quick Reference
| RunUO | ModernUO |
|---|---|
| `[Constructable]` | `[Constructible]` |
| `m_Field` | `_field` |
| `Console.WriteLine(...)` | `logger.Information(...)` |
| `DateTime.UtcNow` | `Core.Now` |
| `MyItem(Serial serial) : base(serial)` | DELETE |
| `lock (_obj) { }` | Remove entirely |
| `ConcurrentDictionary` | `Dictionary` |
| `ArrayPool<T>.Shared` | `STArrayPool<T>.Shared` |
## Anti-Patterns
- Don't rename existing `m_` fields in code you're not otherwise migrating
- Don't add threading constructs -- everything is single-threaded
- Don't use allocating LINQ (`.ToList()`, `.GroupBy()`, etc.) on hot paths
## See Also
- `dev-docs/runuo-migration-docs/01-foundation-changes.md` -- complete foundation changes reference
- `dev-docs/code-standards.md` -- ModernUO coding standards and LINQ tiers
- `dev-docs/threading-model.md` -- Why single-threaded, what's allowed

View file

@ -0,0 +1,45 @@
---
name: migrate-gumps
description: >
Trigger: when converting RunUO Gump classes, OnResponse handlers, or gump UI code to ModernUO DynamicGump/StaticGump.
Covers: builder pattern, DisplayTo, response handling, empty gump rule.
---
# RunUO -> ModernUO Gump Migration
## When This Activates
- Converting `Gump` subclasses
- Converting `OnResponse(NetState, RelayInfo)` handlers
- Updating gump sending/closing patterns
## Conversion Steps
1. Choose type: `DynamicGump` (variable layout) or `StaticGump<T>` (fixed layout)
2. Change class declaration: `class X : Gump` -> `class X : DynamicGump`
3. Add `public override bool Singleton => true;` if only one per player
4. Make constructor private, add static `DisplayTo()` method
5. Move all `AddXxx()` calls from constructor to `BuildLayout(ref DynamicGumpBuilder builder)`
6. Prefix each call with `builder.`: `AddLabel(...)` -> `builder.AddLabel(...)`
7. Convert properties: `Closable = false` -> `builder.SetNoClose()`
8. Update OnResponse: `OnResponse(NetState, RelayInfo)` -> `OnResponse(NetState, in RelayInfo)`
9. Update text entries: `info.TextEntries[i].Text` -> `info.GetTextEntry(id)`
10. For StaticGump: extract variable text into placeholders + `BuildStrings`
## Quick Mapping
| RunUO | ModernUO |
|---|---|
| `class X : Gump` | `class X : DynamicGump` or `StaticGump<X>` |
| `AddPage(0)` | `builder.AddPage()` |
| `Closable = false` | `builder.SetNoClose()` |
| `Dragable = false` | `builder.SetNoMove()` |
| `OnResponse(NetState, RelayInfo)` | `OnResponse(NetState, in RelayInfo)` |
| `info.TextEntries[i].Text` | `info.GetTextEntry(id)` |
| `from.SendGump(new X(...))` | `X.DisplayTo(from, ...)` |
| `from.CloseGump(typeof(X))` | `from.CloseGump<X>()` |
## Critical: Empty Gump Rule
Never create a gump with no visual elements. Use the `DisplayTo()` pattern -- validate before constructing.
## See Also
- `dev-docs/runuo-migration-docs/04-gumps.md` -- detailed migration reference
- `dev-docs/gump-system.md` -- complete ModernUO gump system
- `dev-docs/claude-skills/modernuo-gump-system.md` -- ModernUO gump skill

View file

@ -0,0 +1,43 @@
---
name: migrate-items-mobiles
description: >
Trigger: when converting RunUO Item, Mobile, or BaseCreature subclasses to ModernUO. Most common migration task.
Covers: complete item/creature conversion combining serialization, timers, properties, naming.
---
# RunUO -> ModernUO Item/Mobile/Creature Migration
## When This Activates
- Converting any `Item` subclass from RunUO
- Converting any `Mobile`/`BaseCreature` subclass
- This is the most common migration task -- combines all other systems
## Item Conversion Checklist
1. [ ] Foundation: file-scoped namespace, `using ModernUO.Serialization;`
2. [ ] Class: add `[SerializationGenerator(0, false)]`, 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
6. [ ] `[Constructable]` -> `[Constructible]`
7. [ ] `Name = "text"` -> `public override string DefaultName => "text";`
8. [ ] Timers: nested class -> `Timer.StartTimer()` + `TimerExecutionToken` + `[AfterDeserialization]` + `OnAfterDelete`
9. [ ] Properties: `GetProperties(ObjectPropertyList)` -> `GetProperties(IPropertyList)`, apply string hole rule
10. [ ] Context menus: `List<ContextMenuEntry>` -> `ref PooledRefList<ContextMenuEntry>`
## Creature-Specific Changes
- `[CorpseName("...")]` attribute -> `public override string CorpseName => "...";`
- `BaseCreature(AI, Fight, 10, 1, 0.2, 0.4)` -> `BaseCreature(AI, Fight)` (extra params default)
- `Name = "text"` -> `public override string DefaultName => "text";`
- Expression-bodied overrides: `public override int Meat { get { return 1; } }` -> `public override int Meat => 1;`
## Anti-Patterns
- Using `_field--` instead of `Property--` (bypasses MarkDirty tracking)
- Forgetting `[AfterDeserialization]` for timer restoration
- Forgetting `OnAfterDelete()` for timer cancellation
## See Also
- `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` -- detailed migration with before/after examples
- `dev-docs/content-patterns.md` -- ModernUO content templates
- `dev-docs/claude-skills/modernuo-content-patterns.md` -- ModernUO content skill
- `dev-docs/claude-skills/modernuo-serialization.md` -- serialization patterns
- `dev-docs/claude-skills/modernuo-timers.md` -- timer patterns

View file

@ -0,0 +1,44 @@
---
name: migrate-packets
description: >
Trigger: when converting RunUO Packet subclasses, PacketWriter/PacketReader, or packet handler registration to ModernUO span-based packets.
Covers: outgoing packet conversion, incoming handler conversion, SpanWriter/SpanReader.
---
# RunUO -> ModernUO Packet Migration
## When This Activates
- Converting `Packet` subclasses to static create methods
- Converting `PacketHandlers.Register()` to `IncomingPackets.Register()`
- Converting `PacketWriter`/`PacketReader` to `SpanWriter`/`SpanReader`
## Conversion Steps (Outgoing)
1. Create static class: `public static class OutgoingMyPackets`
2. Define length constant
3. Convert constructor to `static void CreateXxx(Span<byte> buffer, ...)`
4. Replace `m_Stream.Write(x)` with `writer.Write(x)`
5. Create `SendXxx(this NetState ns, ...)` extension method
6. Check `ns.CannotSendPackets()` at start
7. Use `stackalloc byte[length].InitializePacket()` for buffer
8. Replace `ns.Send(new Packet(...))` with `ns.SendXxx(...)`
## Conversion Steps (Incoming)
1. Change `PacketHandlers.Register(id, len, ingame, new OnPacketReceive(H))` to `IncomingPackets.Register(id, len, ingame, &H)` in `unsafe Configure()`
2. Change handler: `void H(NetState, PacketReader)` -> `void H(NetState, SpanReader)`
3. Replace read calls: `pvSrc.ReadString()` -> `reader.ReadAsciiSafe()`
## Quick Mapping
| RunUO | ModernUO |
|---|---|
| `class X : Packet` | Static `CreateX(Span<byte>)` method |
| `m_Stream.Write(val)` | `writer.Write(val)` (SpanWriter) |
| `PacketWriter` | `SpanWriter` |
| `PacketReader` / `pvSrc` | `SpanReader` / `reader` |
| `ns.Send(new X(...))` | `ns.SendX(...)` extension |
| `PacketHandlers.Register(...)` | `IncomingPackets.Register(... &handler)` |
| `new OnPacketReceive(H)` | `&H` (function pointer) |
## See Also
- `dev-docs/runuo-migration-docs/05-packets-networking.md` -- detailed migration reference
- `dev-docs/networking-packets.md` -- complete ModernUO networking system
- `dev-docs/claude-skills/modernuo-networking.md` -- ModernUO networking skill

View file

@ -0,0 +1,42 @@
---
name: migrate-persistence
description: >
Trigger: when converting RunUO EventSink.WorldSave/WorldLoad manual binary persistence to ModernUO GenericPersistence.
Covers: GenericPersistence subclassing, IGenericWriter/IGenericReader, MarkDirty pattern.
---
# RunUO -> ModernUO Persistence Migration
## When This Activates
- Converting `EventSink.WorldSave`/`EventSink.WorldLoad` patterns
- Converting manual `BinaryFileWriter`/`BinaryFileReader` persistence
- Systems that save custom data outside of Item/Mobile serialization
## Conversion Steps
1. Create class inheriting `GenericPersistence`: `class MySystem : GenericPersistence`
2. Add static instance + `Configure()`: `_instance = new MySystem();`
3. Call `base("SaveName", 10)` in constructor
4. Move save logic to `override Serialize(IGenericWriter writer)`
5. Move load logic to `override Deserialize(IGenericReader reader)`
6. Remove `EventSink.WorldSave`/`WorldLoad` subscriptions
7. Remove all file management code (Directory.Create, File.Exists, FileStream)
8. Add `_instance.MarkDirty()` wherever data changes
9. Replace `writer.Write(mobile.Serial.Value)` -> `writer.Write(mobile)`
10. Replace `World.FindMobile(reader.ReadInt32())` -> `reader.ReadEntity<Mobile>()`
## Template
```csharp
public class MySystem : GenericPersistence
{
private static MySystem _instance;
public static void Configure() => _instance = new MySystem();
public MySystem() : base("MySystem", 10) { }
public override void Serialize(IGenericWriter writer) { /* save */ }
public override void Deserialize(IGenericReader reader) { /* load */ }
}
```
## See Also
- `dev-docs/runuo-migration-docs/08-persistence.md` -- detailed migration reference with before/after
- `dev-docs/serialization.md` -- ModernUO serialization system (IGenericWriter/IGenericReader)
- `dev-docs/claude-skills/modernuo-serialization.md` -- ModernUO serialization skill

View file

@ -0,0 +1,33 @@
---
name: migrate-property-lists
description: >
Trigger: when converting RunUO GetProperties(ObjectPropertyList) to ModernUO GetProperties(IPropertyList). Critical string literal rule.
Covers: IPropertyList interface, string hole rule, cliloc arguments.
---
# RunUO -> ModernUO Property List Migration
## When This Activates
- Converting `GetProperties(ObjectPropertyList list)` overrides
- Updating tooltip/property list code
## Conversion Steps
1. Change signature: `GetProperties(ObjectPropertyList list)` -> `GetProperties(IPropertyList list)`
2. Convert string args to interpolation: `list.Add(num, val.ToString())` -> `list.Add(num, $"{val}")`
3. Apply string hole rule: `$"Text\t{val}"` -> `$"{"Text"}\t{val}"`
4. Tab-separated: `string.Format("{0}\t{1}", a, b)` -> `$"{a}\t{b}"`
5. Cliloc as argument: `"#" + cliloc` -> `$"{cliloc:#}"`
## Critical Rule: String Literals Must Be Holes
Only `\t` should be a bare literal. All text must be inside `{}` holes:
```csharp
// BAD: "Map" is a delimiter
list.Add(1060658, $"Map\t{value}");
// GOOD: "Map" is an argument
list.Add(1060658, $"{"Map"}\t{value}");
```
## See Also
- `dev-docs/runuo-migration-docs/06-property-lists.md` -- detailed migration reference
- `dev-docs/property-lists.md` -- complete ModernUO property list system
- `dev-docs/claude-skills/modernuo-property-lists.md` -- ModernUO property list skill

View file

@ -0,0 +1,51 @@
---
name: migrate-serialization
description: >
Trigger: when converting RunUO Serialize/Deserialize methods, adding [SerializableField], converting [Constructable] to [Constructible], or migrating manual serialization code.
Covers: source-generated serialization, field conversion, version handling, TypeAlias.
---
# RunUO -> ModernUO Serialization Migration
## When This Activates
- Converting `Serialize(GenericWriter)`/`Deserialize(GenericReader)` overrides
- Converting `[Constructable]` to `[Constructible]`
- Adding `[SerializableField]` attributes
- Handling save compatibility with `[TypeAlias]`
## 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
## Quick Mapping
| RunUO | ModernUO |
|---|---|
| `public class Foo : Item` | `[SerializationGenerator(0, false)] public partial class Foo : Item` |
| `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 |
| Custom setter with InvalidateProperties() | `[InvalidateProperties]` attribute |
| Custom setter logic | `[SerializableProperty(N)]` with `this.MarkDirty()` |
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` |
| `reader.ReadItem()` | `reader.ReadEntity<Item>()` |
## Anti-Patterns
- Missing `partial` keyword -> build error
- Serializing `TimerExecutionToken` -> build error
- Missing `this.MarkDirty()` in `[SerializableProperty]` setter -> changes not saved
- Wrong field prefix (`m_` instead of `_`)
## See Also
- `dev-docs/runuo-migration-docs/02-serialization.md` -- detailed migration reference with before/after
- `dev-docs/serialization.md` -- complete ModernUO serialization system
- `dev-docs/claude-skills/modernuo-serialization.md` -- ModernUO serialization skill (patterns, attributes, examples)

View file

@ -0,0 +1,45 @@
---
name: migrate-systems
description: >
Trigger: when converting multi-file RunUO engines or systems (crafting, spawners, economy, quests) to ModernUO.
Covers: system mapping, conversion order, file organization, cross-reference handling.
---
# RunUO -> ModernUO Multi-File System Migration
## When This Activates
- Converting RunUO systems with multiple interdependent files
- Converting custom engines (crafting, spawners, economy, quests)
- Organizing RunUO `Scripts/Custom/` code into ModernUO structure
## Conversion Order
1. **Data types / enums** -- Just naming and namespace changes
2. **Persistence classes** -- `EventSink.WorldSave` -> `GenericPersistence`
3. **Core entities (Items/Mobiles)** -- Full serialization migration
4. **Gumps** -- Convert to `DynamicGump`/`StaticGump`
5. **Commands** -- Usually minimal changes
6. **Packets** -- Convert to `SpanWriter` if custom packets exist
7. **Entry point** -- Update Configure/Initialize registration
## File Organization
| RunUO | ModernUO |
|---|---|
| `Scripts/Custom/MySystem/` | `Projects/UOContent/Engines/MySystem/` or `Projects/UOContent/Systems/MySystem/` |
| `Scripts/Items/X.cs` | `Projects/UOContent/Items/{Category}/X.cs` |
| `Scripts/Mobiles/X.cs` | `Projects/UOContent/Mobiles/{Category}/X.cs` |
| `Scripts/Gumps/X.cs` | `Projects/UOContent/Gumps/X.cs` |
## Configuration Migration
| RunUO | ModernUO |
|---|---|
| XML config files | `ServerConfiguration.GetOrUpdateSetting()` or `JsonConfig` |
| Custom .cfg parsing | `ServerConfiguration` for simple, `JsonConfig` for complex |
## Testing
After converting: `dotnet build`, fix errors, test [add for items, verify gumps, check persistence across save/restart.
## See Also
- `dev-docs/runuo-migration-docs/10-systems-engines.md` -- detailed system migration patterns
- `dev-docs/configuration.md` -- ModernUO configuration system
- `dev-docs/claude-skills/modernuo-configuration.md` -- ModernUO configuration skill
- All other migrate-* skills for system-specific guidance

View file

@ -0,0 +1,44 @@
---
name: migrate-timers
description: >
Trigger: when converting RunUO Timer subclasses, Timer.DelayCall patterns, or TimerPriority usage to ModernUO fire-and-forget timers.
Covers: Timer subclass elimination, TimerExecutionToken, callback patterns, timer restoration.
---
# RunUO -> ModernUO Timer Migration
## When This Activates
- Converting nested `Timer` subclasses with `OnTick()`
- Converting `Timer.DelayCall()` patterns
- Removing `TimerPriority` usage
- Restoring timers after deserialization
## Conversion Steps
1. Move `OnTick()` logic to a method on the parent class
2. Replace `new InternalTimer(this).Start()` with `Timer.StartTimer(..., callback, out _token)`
3. Add `private TimerExecutionToken _token;` (NOT serialized)
4. Cancel in `OnAfterDelete()`: `_token.Cancel();`
5. Restore in `[AfterDeserialization]`: re-call `Timer.StartTimer(...)`
6. Delete the nested Timer class entirely
7. Remove all `TimerPriority` references
## Quick Mapping
| RunUO | ModernUO |
|---|---|
| `new Timer(delay).Start()` | `Timer.StartTimer(delay, callback)` |
| `new Timer(delay, interval).Start()` | `Timer.StartTimer(delay, interval, callback, out token)` |
| `timer.Stop()` | `token.Cancel()` |
| `Timer.DelayCall(delay, callback)` | `Timer.StartTimer(delay, callback)` |
| `Timer.DelayCall(delay, stateCallback, state)` | `Timer.DelayCall(delay, callback, state)` |
| `TimerPriority.XXX` | Remove -- timer wheel auto-schedules |
| Timer started in `Deserialize()` | `[AfterDeserialization]` method |
## Anti-Patterns
- Serializing `TimerExecutionToken` -- it's a struct tracking a pooled timer
- Starting timers in `Deserialize()` -- world isn't loaded yet, use `[AfterDeserialization]`
- Lambda closures on hot paths -- use `Timer.DelayCall` with state parameters instead
## See Also
- `dev-docs/runuo-migration-docs/03-timers.md` -- detailed migration reference
- `dev-docs/timers.md` -- complete ModernUO timer system
- `dev-docs/claude-skills/modernuo-timers.md` -- ModernUO timer skill

View file

@ -221,10 +221,52 @@ public partial class BagOfSending : Item
}
```
## Custom Serialize/Deserialize (Purity Rules)
When writing custom `Serialize(IGenericWriter)` or `Deserialize(IGenericReader)` methods (e.g. for `GenericPersistence` subclasses), the following rules apply:
### Serialize() MUST remain pure
`Serialize()` is called from **background serialization threads** during world saves (see `SerializationThreadWorker`). Multiple entities are serialized in parallel across threads. This means `Serialize()` must NOT:
- **Create or destroy Items/Mobiles** -- mutates shared world state
- **Move, equip, or unequip Items/Mobiles** -- mutates shared world state
- **Start or stop timers** (`Timer.StartTimer`, `Timer.DelayCall`, `_token.Cancel()`) -- timers are NOT thread-safe
- **Send packets or modify NetState** -- networking is game-thread-only
- **Access or modify other entities' mutable state** -- data race
- **Call `Delete()`** on anything -- triggers deletion cascades on wrong thread
`Serialize()` should ONLY read fields and write them to the `IGenericWriter`. Treat it as a read-only snapshot.
```csharp
// CORRECT -- pure reads and writes only
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_records.Count);
foreach (var (key, value) in _records)
{
writer.Write(key);
writer.Write(value);
}
}
// WRONG -- side effects in Serialize
public override void Serialize(IGenericWriter writer)
{
CleanupExpiredEntries(); // BAD: mutates state
Timer.StartTimer(Recheck); // BAD: not thread-safe
writer.Write(_data);
}
```
### 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.
## Anti-Patterns
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
- **Serializing timers**: `TimerExecutionToken` cannot be serialized
- **Side effects in `Serialize()`**: Serialize runs on background threads -- must be pure (no creating/destroying entities, no timer start/stop, no packets)
- **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

View file

@ -116,19 +116,32 @@ using var list = PooledRefList<Mobile>.CreateMT();
## World Saves
World saves DO involve background work, but this is handled by server infrastructure:
1. **Serialization** happens on the main thread (safe access to game state)
2. **Disk I/O** may happen on background threads (no game state access)
3. Main thread blocks briefly during serialization snapshot
World saves use **parallel serialization threads**. This is critical to understand:
1. **Preserialize**: Allocates heaps and wakes serialization thread workers (background)
2. **Snapshot**: Main thread calls `Persistence.SerializeAll()` which pushes entities into `SerializationThreadWorker` queues (round-robin). Workers call `Serialize(writer)` on **their own background threads** in parallel.
3. **Write snapshot**: Disk I/O on background threads after serialization completes
```csharp
// From World.cs -- save flow:
World.Save();
→ Preserialize() on thread pool (allocate heaps)
→ Snapshot() on main thread (serialize game state)
→ WriteFiles() on thread pool (disk I/O only)
→ Preserialize() on thread pool (allocate heaps, wake serialization workers)
→ Snapshot() on main thread (queues entities to workers, workers serialize in parallel)
→ SerializationThreadWorker.Execute() calls e.Serialize(writer) on background thread
→ PauseSerializationThreads() (wait for workers to finish)
→ WriteSnapshot() on thread pool (disk I/O only)
```
### Serialize() runs on background threads
Because `SerializationThreadWorker` calls `Serialize()` on its own thread, **`Serialize()` must be pure**:
- **NO** creating/destroying Items or Mobiles
- **NO** starting/stopping timers (not thread-safe)
- **NO** sending packets or modifying NetState
- **NO** mutating shared game state
- **ONLY** read fields and write to `IGenericWriter`
See `modernuo-serialization.md` for full purity rules.
## Exceptions: Server Infrastructure
These files MAY use threading (they're server infrastructure, not game logic):

View file

@ -19,6 +19,7 @@ description: >
3. **Never serialize `TimerExecutionToken`** -- restore in `[AfterDeserialization]`
4. **Always cancel timers in `OnDelete()`/`OnAfterDelete()`**
5. **8ms minimum precision** -- timer wheel uses 8ms tick rate
6. **Timers are NOT thread-safe** -- never Start/Stop timers or call `Timer.DelayCall`/`Timer.StartTimer` from any thread other than the game thread. This includes `Serialize()` which runs on background serialization threads during world saves.
## Preferred APIs (In Order)
@ -193,6 +194,7 @@ _decayTimer = null;
- **Forgetting cleanup**: Timers keep running if not cancelled on deletion
- **Using `Thread.Sleep`**: Blocks the game loop. Use `Timer.StartTimer` or `await Timer.Pause` instead
- **Creating timers in constructors called during deserialization**: Use `[AfterDeserialization]` instead
- **Starting/stopping timers in `Serialize()`**: `Serialize()` runs on background threads during world saves -- timer APIs are game-thread-only and will corrupt state or crash
## Real Examples
- Token cleanup: `Projects/UOContent/Spells/Third/WallOfStone.cs`