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

@ -43,6 +43,8 @@ Apply these when writing or reviewing `.cs` files under `Projects/`.
| Configuration system | `dev-docs/configuration.md` | | Configuration system | `dev-docs/configuration.md` |
| Networking & packets | `dev-docs/networking-packets.md` | | Networking & packets | `dev-docs/networking-packets.md` |
| Region system | `dev-docs/regions.md` | | Region system | `dev-docs/regions.md` |
| RunUO migration (overview) | `dev-docs/runuo-migration-docs/00-overview.md` |
| RunUO migration (all docs) | `dev-docs/runuo-migration-docs/` |
## Claude Skills (Opt-In) ## Claude Skills (Opt-In)
@ -71,7 +73,20 @@ Then copy only the relevant skill files based on the task:
| Era-conditional code | `modernuo-era-expansion` | | Era-conditional code | `modernuo-era-expansion` |
| Code review / audit | `modernuo-code-audit` | | Code review / audit | `modernuo-code-audit` |
| Any `.cs` file edit | `modernuo-code-audit` (always offer for code changes) | | Any `.cs` file edit | `modernuo-code-audit` (always offer for code changes) |
| **RunUO Migration** | |
| Migrate any RunUO script | `migrate-from-runuo/migrate-foundation` (always), plus system-specific skills below |
| Migrate Item/Mobile/Creature | `migrate-from-runuo/migrate-foundation`, `migrate-from-runuo/migrate-serialization`, `migrate-from-runuo/migrate-items-mobiles` |
| Migrate serialization | `migrate-from-runuo/migrate-serialization` |
| Migrate timers | `migrate-from-runuo/migrate-timers` |
| Migrate gumps | `migrate-from-runuo/migrate-gumps` |
| Migrate packets | `migrate-from-runuo/migrate-packets` |
| Migrate property lists | `migrate-from-runuo/migrate-property-lists` |
| Migrate events/commands | `migrate-from-runuo/migrate-commands-events` |
| Migrate persistence (WorldSave) | `migrate-from-runuo/migrate-persistence` |
| Migrate multi-file system | `migrate-from-runuo/migrate-systems` |
To enable a skill: `cp dev-docs/claude-skills/<name>.md .claude/skills/` To enable a skill: `cp dev-docs/claude-skills/<name>.md .claude/skills/`
Migration skills reference the deep docs in `dev-docs/runuo-migration-docs/` and point to existing ModernUO skills for best practices.
The `modernuo-code-audit` skill auto-triggers on `.cs` file edits and flags convention violations (warnings only, asks before fixing). The `modernuo-code-audit` skill auto-triggers on `.cs` file edits and flags convention violations (warnings only, asks before fixing).

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 ## Anti-Patterns
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class` - **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
- **Serializing timers**: `TimerExecutionToken` cannot be serialized - **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()` - **Missing `MarkDirty()`**: Custom property setters must call `this.MarkDirty()`
- **Wrong field prefix**: Use `_camelCase`, not `m_camelCase` for new fields - **Wrong field prefix**: Use `_camelCase`, not `m_camelCase` for new fields
- **Forgetting `[Constructible]`**: Items/Mobiles need this for `[add` command - **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
World saves DO involve background work, but this is handled by server infrastructure: World saves use **parallel serialization threads**. This is critical to understand:
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) 1. **Preserialize**: Allocates heaps and wakes serialization thread workers (background)
3. Main thread blocks briefly during serialization snapshot 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 ```csharp
// From World.cs -- save flow: // From World.cs -- save flow:
World.Save(); World.Save();
→ Preserialize() on thread pool (allocate heaps) → Preserialize() on thread pool (allocate heaps, wake serialization workers)
→ Snapshot() on main thread (serialize game state) → Snapshot() on main thread (queues entities to workers, workers serialize in parallel)
→ WriteFiles() on thread pool (disk I/O only) → 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 ## Exceptions: Server Infrastructure
These files MAY use threading (they're server infrastructure, not game logic): 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]` 3. **Never serialize `TimerExecutionToken`** -- restore in `[AfterDeserialization]`
4. **Always cancel timers in `OnDelete()`/`OnAfterDelete()`** 4. **Always cancel timers in `OnDelete()`/`OnAfterDelete()`**
5. **8ms minimum precision** -- timer wheel uses 8ms tick rate 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) ## Preferred APIs (In Order)
@ -193,6 +194,7 @@ _decayTimer = null;
- **Forgetting cleanup**: Timers keep running if not cancelled on deletion - **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 - **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 - **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 ## Real Examples
- Token cleanup: `Projects/UOContent/Spells/Third/WallOfStone.cs` - Token cleanup: `Projects/UOContent/Spells/Third/WallOfStone.cs`

View file

@ -0,0 +1,68 @@
# Migrating from RunUO to ModernUO — Overview
## Why Migration Is Needed
RunUO 2.7 targets .NET Framework (Windows-only, end-of-life). ModernUO targets .NET 10 (cross-platform, actively supported). Beyond the runtime, ModernUO has rewritten or replaced nearly every major subsystem for performance, safety, and maintainability.
## High-Level Architecture Differences
| Aspect | RunUO 2.7 | ModernUO |
|---|---|---|
| Runtime | .NET Framework 4.x (Windows) | .NET 10 (cross-platform) |
| Project structure | `Server/` + `Scripts/` (dynamic compilation) | `Projects/Server/` + `Projects/UOContent/` (compiled project) |
| Serialization | Manual `Serialize(GenericWriter)`/`Deserialize(GenericReader)` overrides | Source-generated via `[SerializationGenerator]` + `[SerializableField]` |
| Timers | `Timer` subclass instances with `Start()`/`Stop()` | Fire-and-forget `Timer.StartTimer()` + `TimerExecutionToken` |
| Packets | `Packet` class hierarchy with `PacketWriter` | Static `Create*Packet(Span<byte>)` methods + `SpanWriter` |
| Gumps | `Gump` with entry lists in constructor | `StaticGump<T>`/`DynamicGump` with builder pattern |
| Property lists | `ObjectPropertyList` directly | `IPropertyList` interface |
| Persistence | `EventSink.WorldSave`/`WorldLoad` with manual binary files | `GenericPersistence`/`GenericEntityPersistence<T>` |
| Events | `EventSink` with delegate types (`WorldSaveEventHandler`) | `EventSink` with `Action<T>` delegates |
| Configuration | XML/cfg files | JSON (`modernuo.json`, `JsonConfig`) |
| Commands | `CommandSystem.Register` | Same, but handler conventions updated |
| Naming | `m_` private fields | `_camelCase` private fields |
| Logging | `Console.WriteLine` | `LogFactory.GetLogger()``logger.Information()` |
| Threading | Locks, volatile, concurrent collections | Single-threaded game loop (forbidden) |
| Collections | `new List<T>()` everywhere | `PooledRefList<T>`, `STArrayPool<T>` |
## Approach to Migration
1. **Foundation changes first** — namespace style, naming, usings, logging, attributes
2. **Serialization** — the biggest change; convert `Serialize`/`Deserialize` to source-generated
3. **System-specific changes** — timers, gumps, packets, events, persistence
4. **Content fixes** — item/mobile/creature adjustments
5. **Testing** — compile, load world, verify in-game behavior
## How These Docs Are Organized
| Doc | Content |
|---|---|
| `01-foundation-changes.md` | Universal changes for ALL scripts |
| `02-serialization.md` | Source-generated serialization system |
| `03-timers.md` | Timer.StartTimer + TimerExecutionToken |
| `04-gumps.md` | StaticGump/DynamicGump builder pattern |
| `05-packets-networking.md` | SpanWriter/SpanReader packet system |
| `06-property-lists.md` | IPropertyList + string hole rule |
| `07-commands-events.md` | Command registration + EventSink changes |
| `08-persistence.md` | GenericPersistence system |
| `09-items-mobiles-creatures.md` | Content migration patterns |
| `10-systems-engines.md` | Multi-file system migration |
| `11-api-reference.md` | Comprehensive API mapping table |
## Key Terminology Changes
| RunUO Term | ModernUO Term |
|---|---|
| `Scripts/` directory | `Projects/UOContent/` directory |
| `[Constructable]` | `[Constructible]` |
| `GenericWriter` / `GenericReader` | `IGenericWriter` / `IGenericReader` |
| `PacketWriter` / `PacketReader` | `SpanWriter` / `SpanReader` |
| `ObjectPropertyList` (parameter) | `IPropertyList` (parameter) |
| `BinaryFileWriter` | `GenericPersistence` base class |
| `EventSink.WorldSave += new WorldSaveEventHandler(Save)` | `GenericPersistence` subclass |
| `m_FieldName` | `_fieldName` |
## See Also
- ModernUO wiki: [Migrating from RunUO](https://github.com/modernuo/ModernUO/wiki/4.-Migrating-From-RunUO)
- ModernUO dev-docs: `dev-docs/` (system-specific documentation)
- SerializationGenerator: https://github.com/modernuo/SerializationGenerator

View file

@ -0,0 +1,308 @@
# Foundation Changes (All Scripts)
## Overview
These changes apply to every RunUO script being migrated. Apply them first before tackling system-specific changes (serialization, timers, gumps, etc.).
## 1. File-Scoped Namespaces
RunUO uses block-scoped namespaces. ModernUO uses file-scoped (C# 10+):
```csharp
// RunUO
namespace Server.Items
{
public class MyItem : Item
{
// ...
}
}
// ModernUO
namespace Server.Items;
public class MyItem : Item
{
// ...
}
```
## 2. Naming Conventions
### Private Fields
RunUO uses `m_` prefix. ModernUO uses `_camelCase`:
```csharp
// RunUO
private int m_Charges;
private Mobile m_Owner;
private string m_Name;
// ModernUO
private int _charges;
private Mobile _owner;
private string _name;
```
**Note**: Don't rename existing `m_` fields in legacy code you're not otherwise modifying. Only use `_` for new code and code you're actively migrating.
### Properties
Both use `PascalCase`. RunUO often has verbose syntax:
```csharp
// RunUO
public int Charges { get { return m_Charges; } set { m_Charges = value; } }
// ModernUO (expression-bodied or auto-property)
public int Charges { get => _charges; set => _charges = value; }
// Or if serialized, [SerializableField] generates it automatically
```
## 3. [Constructable] → [Constructible]
Spelling change for the attribute on parameterless constructors:
```csharp
// RunUO
[Constructable]
public MyItem() : base(0x1234) { }
// ModernUO
[Constructible]
public MyItem() : base(0x1234) { }
```
The `using` also changes:
```csharp
// RunUO — no using needed, it's in Server namespace
// ModernUO — still in Server namespace, but ensure you have:
using ModernUO.Serialization; // for [SerializationGenerator], [SerializableField]
```
## 4. Logging
`Console.WriteLine` is never used in ModernUO. Use structured logging:
```csharp
// RunUO
Console.WriteLine("Player {0} logged in from {1}", name, ip);
// ModernUO
using Server.Logging;
private static readonly ILogger logger = LogFactory.GetLogger(typeof(MyClass));
logger.Information("Player {Name} logged in from {IP}", name, ip);
```
Log levels: `Debug`, `Information`, `Warning`, `Error`, `Fatal`
## 5. DateTime.UtcNow → Core.Now
```csharp
// RunUO
DateTime.UtcNow
// ModernUO
Core.Now
```
`Core.Now` is the server's authoritative time source, consistent within each game tick.
## 6. World Iteration → Spatial Queries
Never iterate `World.Mobiles` or `World.Items`. Use map-based spatial queries:
```csharp
// RunUO
foreach (Mobile m in World.Mobiles.Values)
{
if (m.InRange(location, 10))
DoSomething(m);
}
// ModernUO
foreach (var m in map.GetMobilesInRange<Mobile>(location, 10))
{
DoSomething(m);
}
```
Available spatial queries (on `Map`):
- `GetMobilesAt<T>(Point3D)` — exact location
- `GetMobilesInRange<T>(Point3D, int range)` — within range
- `GetMobilesInBounds<T>(Rectangle2D)` — within rectangle
- Same for `GetItemsAt`, `GetItemsInRange`, `GetItemsInBounds`
## 7. Remove Concurrency Primitives
ModernUO's game loop is single-threaded. Remove all threading constructs:
```csharp
// RunUO (remove ALL of these)
lock (_syncObj) { }
volatile int _counter;
ConcurrentDictionary<int, Item> _items;
Mutex m = new Mutex();
Semaphore s = new Semaphore(1, 1);
// ModernUO (single-threaded replacements)
// lock → remove entirely
// volatile → remove keyword
// ConcurrentDictionary → Dictionary
// Mutex/Semaphore → remove entirely
```
## 8. No Task.Run / new Thread
Game code must not spawn threads:
```csharp
// RunUO (FORBIDDEN in ModernUO)
Task.Run(() => ProcessItems());
new Thread(BackgroundWork).Start();
ThreadPool.QueueUserWorkItem(Work);
// ModernUO — use timers or await
Timer.StartTimer(TimeSpan.FromSeconds(1), ProcessItems);
await Timer.Pause(1000); // for async/await patterns
```
## 9. ArrayPool → STArrayPool
```csharp
// RunUO
var buffer = ArrayPool<byte>.Shared.Rent(1024);
// ...
ArrayPool<byte>.Shared.Return(buffer);
// ModernUO (single-threaded, no locks)
var buffer = STArrayPool<byte>.Shared.Rent(1024);
// ...
STArrayPool<byte>.Shared.Return(buffer);
```
## 10. new List<T>() on Hot Paths → PooledRefList<T>
```csharp
// RunUO
var list = new List<Mobile>();
foreach (var m in nearbyMobiles)
{
if (m.Alive)
list.Add(m);
}
// list goes to GC
// ModernUO (zero-alloc)
using var list = PooledRefList<Mobile>.Create();
foreach (var m in nearbyMobiles)
{
if (m.Alive)
list.Add(m);
}
// Automatically returns array to pool on Dispose
```
## 11. LINQ Restrictions
ModernUO has tiered LINQ rules. On hot paths:
- **Tier 1 (allowed)**: `foreach` over `IEnumerable<T>`, `.Contains()` after LINQ operators, `.OrderBy().First()`, `.Count()` on sized collections
- **Tier 2 (acceptable on warm paths)**: `.Skip().Take().ToArray()`, `.Where()` on arrays
- **Tier 3 (forbidden on hot paths)**: `.Select().Where()` chains, `.GroupBy()`, `.ToDictionary()`, `.Aggregate()`, `.Sum()`/`.Min()`/`.Max()`
```csharp
// RunUO (common LINQ patterns — FORBIDDEN on hot paths in ModernUO)
var targets = nearbyMobiles.Where(m => m.Alive).ToList();
var count = items.Count(i => i.Stackable);
// ModernUO (manual loops)
using var targets = PooledRefList<Mobile>.Create();
foreach (var m in nearbyMobiles)
{
if (m.Alive)
targets.Add(m);
}
var count = 0;
foreach (var i in items)
{
if (i.Stackable)
count++;
}
```
See `dev-docs/code-standards.md` for full LINQ tier details.
## 12. Property Syntax Modernization
```csharp
// RunUO (verbose)
public int Charges
{
get { return m_Charges; }
set { m_Charges = value; }
}
public override string Name
{
get { return "An Item"; }
}
// ModernUO (modern C#)
public int Charges { get => _charges; set => _charges = value; }
public override string DefaultName => "an item";
```
## 13. Using Directives
Common new usings in ModernUO:
```csharp
using ModernUO.Serialization; // [SerializationGenerator], [SerializableField], etc.
using Server.Logging; // ILogger, LogFactory
using Server.Gumps; // SendGump, HasGump, etc. extension methods
using Server.Collections; // PooledRefList
```
Removed/changed usings:
```csharp
// RunUO (no longer exists/changed)
using Server.Network; // Packet classes removed — use extension methods
```
## 14. Serial Constructor Removal
RunUO items have a deserialization constructor `MyItem(Serial serial) : base(serial)`. In ModernUO with `[SerializationGenerator]`, this constructor is generated automatically. **Remove it.**
```csharp
// RunUO
public MyItem(Serial serial) : base(serial) { }
// ModernUO — DELETE THIS CONSTRUCTOR. The source generator creates it.
```
## Quick Checklist
When migrating any RunUO script, apply these changes in order:
1. [ ] Change to file-scoped namespace
2. [ ] Add `using ModernUO.Serialization;`
3. [ ] Rename `m_` fields to `_camelCase`
4. [ ] Change `[Constructable]` to `[Constructible]`
5. [ ] Replace `Console.WriteLine` with structured logging
6. [ ] Replace `DateTime.UtcNow` with `Core.Now`
7. [ ] Replace `World.Mobiles`/`World.Items` iteration with spatial queries
8. [ ] Remove concurrency primitives
9. [ ] Remove threading code
10. [ ] Replace `ArrayPool` with `STArrayPool`
11. [ ] Replace `new List<T>()` on hot paths with `PooledRefList<T>`
12. [ ] Modernize property syntax
13. [ ] Remove `Serial` constructor (handled by serialization generator)
14. [ ] Update usings
## See Also
- `dev-docs/code-standards.md` — Full coding standards and LINQ tiers
- `dev-docs/threading-model.md` — Threading model details
- `02-serialization.md` — Next step: converting serialization

View file

@ -0,0 +1,423 @@
# Serialization Migration
## Overview
This is the most impactful migration change. RunUO uses manual `Serialize(GenericWriter)`/`Deserialize(GenericReader)` overrides. ModernUO uses a source generator that automatically produces serialization code from attribute-decorated fields.
## RunUO Pattern
```csharp
using Server;
namespace Server.Items
{
public class ChargedGem : Item
{
private int m_Charges;
private Mobile m_Owner;
[CommandProperty(AccessLevel.GameMaster)]
public int Charges { get { return m_Charges; } set { m_Charges = value; InvalidateProperties(); } }
[CommandProperty(AccessLevel.GameMaster)]
public Mobile Owner { get { return m_Owner; } set { m_Owner = value; } }
[Constructable]
public ChargedGem() : base(0x1EA7)
{
m_Charges = 10;
Weight = 1.0;
}
public ChargedGem(Serial serial) : base(serial) { }
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)1); // version
// Version 1
writer.Write(m_Owner);
// Version 0
writer.Write(m_Charges);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
switch (version)
{
case 1:
{
m_Owner = reader.ReadMobile();
goto case 0;
}
case 0:
{
m_Charges = reader.ReadInt();
break;
}
}
}
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString()); // charges: ~1_val~
}
}
}
```
## ModernUO Equivalent
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class ChargedGem : Item
{
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private Mobile _owner;
[Constructible]
public ChargedGem() : base(0x1EA7)
{
_charges = 10;
Weight = 1.0;
}
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}"); // charges: ~1_val~
}
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `public class Foo : Item` | `public partial class Foo : Item` | Must add `partial` |
| `[Constructable]` | `[Constructible]` | Spelling change |
| `Foo(Serial serial) : base(serial)` | DELETE | Generated automatically |
| `Serialize(GenericWriter writer)` | DELETE | Generated from `[SerializableField]` attributes |
| `Deserialize(GenericReader reader)` | DELETE | Generated from attributes |
| `writer.Write((int)version)` | `[SerializationGenerator(version, false)]` | Version in attribute |
| `private int m_Charges` | `[SerializableField(0)] private int _charges` | Attribute + rename |
| `[CommandProperty(AccessLevel.GM)]` on property | `[SerializedCommandProperty(AccessLevel.GM)]` on field | Moves to field |
| `InvalidateProperties()` in setter | `[InvalidateProperties]` on field | Attribute replaces manual call |
| `GenericWriter` | `IGenericWriter` | Interface now |
| `GenericReader` | `IGenericReader` | Interface now |
| `reader.ReadInt()` | `reader.ReadInt()` | Same for manual cases |
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` | Generic method |
| `reader.ReadItem()` | `reader.ReadEntity<Item>()` | Generic method |
| `writer.Write((int)0)` version | `[SerializationGenerator(0, false)]` | In attribute |
## Step-by-Step Conversion
### Step 1: Add Required Using
```csharp
using ModernUO.Serialization;
```
### Step 2: Add Class Attributes and `partial`
```csharp
// Change:
public class MyItem : Item
// To:
[SerializationGenerator(0, 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.
### Step 4: Convert Fields
For each field that was serialized in `Serialize()`:
```csharp
// RunUO
private int m_Charges;
[CommandProperty(AccessLevel.GameMaster)]
public int Charges { get { return m_Charges; } set { m_Charges = value; } }
// ModernUO
[SerializableField(0)] // Index = serialization order
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
// Property is auto-generated: public int Charges { get; set; }
```
Add `[InvalidateProperties]` if the RunUO setter called `InvalidateProperties()`:
```csharp
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
```
### Step 5: Delete Serialize and Deserialize Methods
Remove both override methods entirely. The source generator creates them.
### Step 6: Change [Constructable] to [Constructible]
```csharp
[Constructible]
public MyItem() : base(0x1234) { }
```
### Step 7: Handle Timer Fields
`TimerExecutionToken` MUST NOT have `[SerializableField]`. Restore timers in `[AfterDeserialization]`:
```csharp
private TimerExecutionToken _timerToken; // NOT serialized
[AfterDeserialization]
private void AfterDeserialization()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), CheckExpiry, out _timerToken);
}
public override void OnAfterDelete()
{
_timerToken.Cancel();
base.OnAfterDelete();
}
```
### Step 8: Handle Custom Property Logic
If a property has non-trivial getter/setter logic, use `[SerializableProperty]` instead:
```csharp
[SerializableProperty(0)]
[CommandProperty(AccessLevel.GameMaster)]
public int MaxItems
{
get => _maxItems == -1 ? DefaultMaxItems : _maxItems;
set
{
_maxItems = value;
InvalidateProperties();
this.MarkDirty(); // REQUIRED in custom setters
}
}
```
### Step 9: Update GetProperties
Change `ObjectPropertyList` to `IPropertyList`:
```csharp
// RunUO
public override void GetProperties(ObjectPropertyList list)
// ModernUO
public override void GetProperties(IPropertyList list)
```
## Before/After Examples
### Simple Item (No Custom Fields)
**RunUO:**
```csharp
namespace Server.Items
{
public class SimpleGem : Item
{
[Constructable]
public SimpleGem() : base(0x1EA7)
{
Weight = 1.0;
}
public SimpleGem(Serial serial) : base(serial) { }
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)0);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
}
}
}
```
**ModernUO:**
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class SimpleGem : Item
{
[Constructible]
public SimpleGem() : base(0x1EA7)
{
Weight = 1.0;
}
public override string DefaultName => "a simple gem";
}
```
### Versioned Item
**RunUO (version 2 — added Owner in v1, Quality in v2):**
```csharp
namespace Server.Items
{
public class MagicGem : Item
{
private int m_Charges;
private Mobile m_Owner;
private GemQuality m_Quality;
[Constructable]
public MagicGem() : base(0x1EA7)
{
m_Charges = 10;
m_Quality = GemQuality.Rough;
}
public MagicGem(Serial serial) : base(serial) { }
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)2);
writer.Write((int)m_Quality);
writer.Write(m_Owner);
writer.Write(m_Charges);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
switch (version)
{
case 2:
m_Quality = (GemQuality)reader.ReadInt();
goto case 1;
case 1:
m_Owner = reader.ReadMobile();
goto case 0;
case 0:
m_Charges = reader.ReadInt();
break;
}
}
}
}
```
**ModernUO:**
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)] // Version 0 — new schema
public partial class MagicGem : Item
{
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private Mobile _owner;
[SerializableField(2)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private GemQuality _quality;
[Constructible]
public MagicGem() : base(0x1EA7)
{
_charges = 10;
_quality = GemQuality.Rough;
}
}
```
**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.
## Edge Cases & Gotchas
### 1. Save Compatibility
ModernUO's serialization format is completely different from RunUO's. You CANNOT load RunUO saves directly into ModernUO with source-generated serialization. Options:
- Use `[TypeAlias("Old.Namespace.ClassName")]` to map old type names
- Start with a fresh world
- Write a one-time migration tool
### 2. MarkDirty() in Custom Setters
If you use `[SerializableProperty]` with a custom setter, you MUST call `this.MarkDirty()`:
```csharp
set
{
_value = value;
this.MarkDirty(); // Required!
}
```
Without this, changes won't be saved.
### 3. Field Ordering
The `[SerializableField(N)]` index determines serialization order. Choose a logical order and don't change it after the first save — or increment the version.
### 4. Conditional Serialization
Use `[SerializableFieldSaveFlag]` and `[SerializableFieldDefault]` to skip default values:
```csharp
[SerializableFieldSaveFlag(0)]
private bool ShouldSerializeMaxItems() => _maxItems != -1;
[SerializableFieldDefault(0)]
private int MaxItemsDefaultValue() => -1;
```
### 5. Collection Fields
Use `[Tidy]` to auto-clean null/deleted entries on deserialization:
```csharp
[Tidy]
[SerializableField(0)]
private List<Mobile> _followers;
```
### 6. DateTime Fields
Use `[DeltaDateTime]` to survive server restarts:
```csharp
[DeltaDateTime]
[SerializableField(0)]
private DateTime _expireTime;
```
### 7. Keeping Manual Serialization (Rare)
Some edge cases still need manual serialization. If a type has complex conditional logic that can't be expressed with attributes, you can implement `ISerializable` manually. But this is rare — try attributes first.
## See Also
- `dev-docs/serialization.md` — Complete ModernUO serialization reference
- `01-foundation-changes.md` — Foundation changes to apply first
- `03-timers.md` — Timer migration (often coupled with serialization)

View file

@ -0,0 +1,327 @@
# Timer Migration
## Overview
RunUO uses `Timer` subclass instances that you construct, start, and stop. ModernUO replaces most of this with fire-and-forget `Timer.StartTimer()` calls and lightweight `TimerExecutionToken` structs for cancellation. The `TimerPriority` enum is removed — ModernUO's timer wheel handles scheduling automatically with 8ms precision.
## RunUO Pattern
```csharp
// RunUO — Timer subclass pattern
public class MyItem : Item
{
private InternalTimer m_Timer;
[Constructable]
public MyItem() : base(0x1234)
{
m_Timer = new InternalTimer(this);
m_Timer.Start();
}
public MyItem(Serial serial) : base(serial) { }
public override void OnDelete()
{
if (m_Timer != null)
m_Timer.Stop();
}
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)0);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
m_Timer = new InternalTimer(this);
m_Timer.Start();
}
private void DoWork()
{
// Timer callback logic
}
private class InternalTimer : Timer
{
private MyItem m_Item;
public InternalTimer(MyItem item) : base(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5))
{
m_Item = item;
Priority = TimerPriority.OneSecond;
}
protected override void OnTick()
{
m_Item.DoWork();
}
}
}
```
### RunUO Timer.DelayCall
```csharp
// One-shot delay
Timer.DelayCall(TimeSpan.FromSeconds(5), new TimerCallback(DoWork));
Timer.DelayCall(TimeSpan.FromSeconds(5), new TimerStateCallback(DoWork), target);
// Repeating
Timer.DelayCall(TimeSpan.Zero, TimeSpan.FromSeconds(1), new TimerCallback(DoWork));
```
## ModernUO Equivalent
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class MyItem : Item
{
private TimerExecutionToken _timerToken;
[Constructible]
public MyItem() : base(0x1234)
{
StartTimer();
}
private void StartTimer()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), DoWork, out _timerToken);
}
[AfterDeserialization]
private void AfterDeserialization() => StartTimer();
public override void OnAfterDelete()
{
_timerToken.Cancel();
base.OnAfterDelete();
}
private void DoWork()
{
// Timer callback logic
}
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `new InternalTimer().Start()` | `Timer.StartTimer(..., callback, out token)` | Fire-and-forget |
| `Timer` subclass with `OnTick()` | Static callback method | No class needed |
| `timer.Stop()` | `_token.Cancel()` | Lightweight struct |
| `timer.Running` | `_token.Running` | Same concept |
| `TimerPriority.OneSecond` | (removed) | Timer wheel handles scheduling |
| `TimerPriority.FiveSeconds` | (removed) | Timer wheel handles scheduling |
| `Timer.DelayCall(delay, callback)` | `Timer.StartTimer(delay, callback)` | Similar API |
| `Timer.DelayCall(delay, callback, state)` | `Timer.DelayCall(delay, callback, state)` | State-carrying version still exists |
| `new TimerCallback(Method)` | `Method` | Direct method reference |
| `new TimerStateCallback(Method)` | Use state-carrying overload | `Timer.DelayCall(delay, Method, arg1, arg2)` |
| Timer started in `Deserialize()` | `[AfterDeserialization]` method | Never start timers in deserialization |
| `m_Timer != null` check | `_token.Running` check | Token is a value type, always valid |
## Step-by-Step Conversion
### Step 1: Identify the Timer Pattern
Look for:
- Nested `Timer` subclass with `OnTick()` override
- `Timer.DelayCall()` calls
- `TimerPriority` usage
### Step 2: Extract the Callback
Move the `OnTick()` logic to a regular method on the parent class:
```csharp
// RunUO — nested class
private class InternalTimer : Timer
{
private MyItem m_Item;
public InternalTimer(MyItem item) : base(TimeSpan.FromSeconds(5)) { m_Item = item; }
protected override void OnTick() { m_Item.DoWork(); }
}
// ModernUO — just the method
private void DoWork()
{
// Same logic, directly on the item
}
```
### Step 3: Replace Construction with Timer.StartTimer
```csharp
// RunUO
m_Timer = new InternalTimer(this);
m_Timer.Start();
// ModernUO (one-shot)
Timer.StartTimer(TimeSpan.FromSeconds(5), DoWork);
// ModernUO (repeating, need cancellation)
Timer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), DoWork, out _timerToken);
// ModernUO (repeating with count limit)
Timer.StartTimer(TimeSpan.Zero, TimeSpan.FromSeconds(1), 10, DoWork, out _timerToken);
```
### Step 4: Add TimerExecutionToken Field (if cancellable)
```csharp
private TimerExecutionToken _timerToken; // NOT serialized — no [SerializableField]
```
### Step 5: Cancel in OnAfterDelete
```csharp
public override void OnAfterDelete()
{
_timerToken.Cancel(); // Safe to call multiple times
base.OnAfterDelete();
}
```
### Step 6: Restore in [AfterDeserialization]
```csharp
[AfterDeserialization]
private void AfterDeserialization()
{
Timer.StartTimer(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(5), DoWork, out _timerToken);
}
```
### Step 7: Delete the Nested Timer Class
Remove the entire `private class InternalTimer : Timer { ... }` block.
### Step 8: Remove TimerPriority
Delete any `Priority = TimerPriority.xxx` lines. The timer wheel handles scheduling.
## Before/After Examples
### Simple One-Shot Timer
**RunUO:**
```csharp
Timer.DelayCall(TimeSpan.FromSeconds(10), new TimerCallback(Delete));
```
**ModernUO:**
```csharp
Timer.StartTimer(TimeSpan.FromSeconds(10), Delete);
```
### Repeating Timer with State
**RunUO:**
```csharp
private class HealTimer : Timer
{
private Mobile m_Target;
public HealTimer(Mobile target) : base(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2))
{
m_Target = target;
Priority = TimerPriority.TwoFiftyMS;
}
protected override void OnTick()
{
if (m_Target.Alive && m_Target.Hits < m_Target.HitsMax)
m_Target.Hits += 5;
else
Stop();
}
}
```
**ModernUO:**
```csharp
private TimerExecutionToken _healTimer;
private void StartHeal(Mobile target)
{
Timer.StartTimer(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2), () => HealTick(target), out _healTimer);
}
private void HealTick(Mobile target)
{
if (target.Alive && target.Hits < target.HitsMax)
target.Hits += 5;
else
_healTimer.Cancel();
}
```
Or for zero-allocation, use the state-carrying `Timer.DelayCall`:
```csharp
Timer.DelayCall(TimeSpan.FromSeconds(2), HealTick, target);
```
### Timer.DelayCall with State
**RunUO:**
```csharp
Timer.DelayCall(TimeSpan.FromSeconds(2), new TimerStateCallback(ProcessTarget), target);
private static void ProcessTarget(object state)
{
Mobile target = (Mobile)state;
// ...
}
```
**ModernUO:**
```csharp
Timer.DelayCall(TimeSpan.FromSeconds(2), ProcessTarget, target);
private static void ProcessTarget(Mobile target)
{
// Type-safe — no casting needed
}
```
ModernUO supports up to 5 typed state parameters:
```csharp
Timer.DelayCall(TimeSpan.FromSeconds(2), ProcessTarget, mobile, item);
Timer.DelayCall(TimeSpan.FromSeconds(2), DoWork, arg1, arg2, arg3);
```
## Edge Cases & Gotchas
### 1. TimerExecutionToken Is NOT Serializable
Never add `[SerializableField]` to a `TimerExecutionToken`. It's a struct that tracks a pooled timer — it can't survive serialization. Always restore timers in `[AfterDeserialization]`.
### 2. Don't Start Timers in Deserialization
In RunUO, timers are commonly started in `Deserialize()`. In ModernUO, use `[AfterDeserialization]` — this runs after the world is fully loaded.
### 3. Cancel() Is Always Safe
`_token.Cancel()` can be called on a default token, a stopped token, or an already-cancelled token. No null checks needed.
### 4. Timer.DelayCall Still Exists
`Timer.DelayCall()` is still available and returns a `Timer` object. Use it when you need the `Timer` reference (e.g., for `[DeserializeTimerField]`) or state-carrying overloads.
### 5. Custom Timer Classes Are Still Possible
For complex timer logic (e.g., `Corpse.DecayTimer`), you can still subclass `Timer` with `OnTick()`. But prefer the fire-and-forget pattern for simple cases.
### 6. Avoid Lambda on Hot Paths
Lambdas allocate closures. For hot-path timers, use state-carrying `Timer.DelayCall` or direct method references:
```csharp
// Allocates closure:
Timer.StartTimer(TimeSpan.FromSeconds(2), () => ProcessTarget(from, target));
// No allocation:
Timer.DelayCall(TimeSpan.FromSeconds(2), ProcessTarget, from, target);
```
## See Also
- `dev-docs/timers.md` — Complete ModernUO timer reference
- `02-serialization.md` — Serialization (timer fields, [AfterDeserialization])
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,343 @@
# Gump Migration
## Overview
RunUO uses a `Gump` base class where layout is built in the constructor using `AddXxx()` entry methods. ModernUO provides `StaticGump<T>` (cached layout) and `DynamicGump` (rebuilt per-instance) with ref struct builders, and enforces the empty gump rule.
## RunUO Pattern
```csharp
using Server.Gumps;
using Server.Network;
namespace Server.Gumps
{
public class ConfirmGump : Gump
{
private Mobile m_From;
private string m_Message;
public ConfirmGump(Mobile from, string message) : base(50, 50)
{
m_From = from;
m_Message = message;
Closable = true;
Disposable = true;
Dragable = true;
Resizable = false;
AddPage(0);
AddBackground(0, 0, 400, 300, 5054);
AddAlphaRegion(10, 10, 380, 280);
AddHtml(20, 20, 360, 200, message, true, true);
AddButton(100, 260, 4005, 4007, 1, GumpButtonType.Reply, 0);
AddHtmlLocalized(135, 262, 100, 20, 1011036, false, false); // OK
AddButton(250, 260, 4017, 4019, 0, GumpButtonType.Reply, 0);
AddHtmlLocalized(285, 262, 100, 20, 1011012, false, false); // Cancel
}
public override void OnResponse(NetState sender, RelayInfo info)
{
if (info.ButtonID == 1)
{
m_From.SendMessage("Confirmed!");
}
}
}
}
// Usage:
from.SendGump(new ConfirmGump(from, "Are you sure?"));
```
## ModernUO Equivalent (DynamicGump)
```csharp
using Server.Gumps;
namespace Server.Gumps;
public class ConfirmGump : DynamicGump
{
private readonly Mobile _from;
private readonly string _message;
public override bool Singleton => true;
private ConfirmGump(Mobile from, string message) : base(50, 50)
{
_from = from;
_message = message;
}
protected override void BuildLayout(ref DynamicGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(0, 0, 400, 300, 5054);
builder.AddAlphaRegion(10, 10, 380, 280);
builder.AddHtml(20, 20, 360, 200, _message, background: true, scrollbar: true);
builder.AddButton(100, 260, 4005, 4007, 1);
builder.AddHtmlLocalized(135, 262, 100, 20, 1011036); // OK
builder.AddButton(250, 260, 4017, 4019, 0);
builder.AddHtmlLocalized(285, 262, 100, 20, 1011012); // Cancel
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID == 1)
{
_from.SendMessage("Confirmed!");
}
}
public static void DisplayTo(Mobile from, string message)
{
if (from?.NetState == null)
return;
from.SendGump(new ConfirmGump(from, message));
}
}
// Usage:
ConfirmGump.DisplayTo(from, "Are you sure?");
```
## ModernUO Equivalent (StaticGump — for fixed layouts)
```csharp
using Server.Gumps;
namespace Server.Gumps;
public class ConfirmGump : StaticGump<ConfirmGump>
{
private readonly Mobile _from;
private readonly string _message;
public override bool Singleton => true;
private ConfirmGump(Mobile from, string message) : base(50, 50)
{
_from = from;
_message = message;
}
protected override void BuildLayout(ref StaticGumpBuilder builder)
{
builder.AddPage();
builder.AddBackground(0, 0, 400, 300, 5054);
builder.AddAlphaRegion(10, 10, 380, 280);
builder.AddHtmlPlaceholder(20, 20, 360, 200, "message", true, true);
builder.AddButton(100, 260, 4005, 4007, 1);
builder.AddHtmlLocalized(135, 262, 100, 20, 1011036);
builder.AddButton(250, 260, 4017, 4019, 0);
builder.AddHtmlLocalized(285, 262, 100, 20, 1011012);
}
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
builder.SetStringSlot("message", _message);
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID == 1)
_from.SendMessage("Confirmed!");
}
public static void DisplayTo(Mobile from, string message)
{
if (from?.NetState == null)
return;
from.SendGump(new ConfirmGump(from, message));
}
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `class Foo : Gump` | `class Foo : DynamicGump` or `class Foo : StaticGump<Foo>` | Choose based on layout |
| Layout in constructor | Layout in `BuildLayout(ref builder)` | Move all AddXxx calls |
| `AddPage(0)` | `builder.AddPage()` | 0 is default |
| `AddBackground(...)` | `builder.AddBackground(...)` | Same args, on builder |
| `AddButton(x, y, n, p, id, GumpButtonType.Reply, 0)` | `builder.AddButton(x, y, n, p, id)` | Simplified — Reply is default |
| `AddButton(x, y, n, p, id, GumpButtonType.Page, p)` | `builder.AddButton(x, y, n, p, id, GumpButtonType.Page, p)` | Same for page nav |
| `AddHtml(x, y, w, h, text, bg, scroll)` | `builder.AddHtml(x, y, w, h, text, background: bg, scrollbar: scroll)` | Named params |
| `AddHtmlLocalized(x, y, w, h, num, bg, scroll)` | `builder.AddHtmlLocalized(x, y, w, h, num)` | Simplified |
| `AddLabel(x, y, hue, text)` | `builder.AddLabel(x, y, hue, text)` | Same |
| `AddTextEntry(x, y, w, h, hue, id, text)` | `builder.AddTextEntry(x, y, w, h, hue, id, text)` | Same |
| `AddCheck(x, y, off, on, state, id)` | `builder.AddCheckbox(x, y, off, on, state, id)` | Renamed |
| `AddRadio(x, y, off, on, state, id)` | `builder.AddRadio(x, y, off, on, state, id)` | Same |
| `Closable = false` | `builder.SetNoClose()` | Property → method |
| `Dragable = false` | `builder.SetNoMove()` | Property → method (note: Dragable → NoMove) |
| `Resizable = false` | `builder.SetNoResize()` | Property → method |
| `Disposable = false` | `builder.SetNoDispose()` | Property → method |
| `OnResponse(NetState, RelayInfo)` | `OnResponse(NetState, in RelayInfo)` | `in` keyword added |
| `info.TextEntries[i].Text` | `info.GetTextEntry(id)` | Direct lookup by ID |
| `info.Switches` contains check | `info.IsSwitched(switchID)` | Direct boolean check |
| `from.SendGump(new MyGump(...))` | `MyGump.DisplayTo(from, ...)` | Static entry point |
| `from.CloseGump(typeof(MyGump))` | `from.CloseGump<MyGump>()` | Generic method |
| `from.HasGump(typeof(MyGump))` | `from.HasGump<MyGump>()` | Generic method |
## Step-by-Step Conversion
### Step 1: Choose Target Type
| If the layout... | Convert to |
|---|---|
| Is the same structure every time | `StaticGump<T>` (best performance) |
| Changes based on data (loops, conditionals) | `DynamicGump` |
| You're unsure | `DynamicGump` (simpler, still much better than legacy `Gump`) |
### Step 2: Change Class Declaration
```csharp
// RunUO
public class MyGump : Gump
// ModernUO
public class MyGump : DynamicGump
// OR
public class MyGump : StaticGump<MyGump>
```
### Step 3: Add Singleton If Needed
```csharp
public override bool Singleton => true; // Only one instance per player
```
### Step 4: Make Constructor Private, Add DisplayTo
```csharp
private MyGump(Mobile from) : base(50, 50) { _from = from; }
public static void DisplayTo(Mobile from)
{
if (from?.NetState == null)
return;
from.SendGump(new MyGump(from));
}
```
### Step 5: Move Layout to BuildLayout
Move all `AddXxx()` calls from the constructor to `BuildLayout(ref DynamicGumpBuilder builder)`, prefixing each with `builder.`.
### Step 6: Convert Properties to Builder Methods
```csharp
// RunUO properties in constructor
Closable = false;
Dragable = false;
// ModernUO methods in BuildLayout
builder.SetNoClose();
builder.SetNoMove();
```
### Step 7: Update OnResponse Signature
```csharp
// RunUO
public override void OnResponse(NetState sender, RelayInfo info)
// ModernUO
public override void OnResponse(NetState sender, in RelayInfo info)
```
### Step 8: Update Text Entry and Switch Access
```csharp
// RunUO
TextRelay relay = info.GetTextEntry(0);
string text = relay != null ? relay.Text : "";
// ModernUO
string text = info.GetTextEntry(0);
```
```csharp
// RunUO
bool isChecked = info.IsSwitched(switchID);
// ModernUO — same
bool isChecked = info.IsSwitched(switchID);
```
### Step 9: For StaticGump — Extract Dynamic Text
Replace variable text with placeholders:
```csharp
// In BuildLayout:
builder.AddLabelPlaceholder(x, y, hue, "playerName");
builder.AddHtmlPlaceholder(x, y, w, h, "description", false, true);
// In BuildStrings:
protected override void BuildStrings(ref GumpStringsBuilder builder)
{
builder.SetStringSlot("playerName", _from.Name);
builder.SetHtmlText("description", _text, "#FFC000", 4);
}
```
### Step 10: Update Callers
```csharp
// RunUO
from.SendGump(new MyGump(from));
from.CloseGump(typeof(MyGump));
// ModernUO
MyGump.DisplayTo(from);
from.CloseGump<MyGump>();
```
## Empty Gump Rule (CRITICAL)
**NEVER send a gump with no visual components.** An empty gump has no close button and cannot be dismissed — it leaks on both client and server.
This typically happens when constructor logic short-circuits:
```csharp
// BAD — gump is created empty, then sent
public MyGump(Mobile from) : base(50, 50)
{
if (!from.Alive)
return; // Empty gump created!
AddPage(0);
// ...
}
```
**Fix**: Use the static `DisplayTo()` pattern. Validate before constructing:
```csharp
private MyGump(Mobile from) : base(50, 50) { /* always builds layout */ }
public static void DisplayTo(Mobile from)
{
if (!from.Alive || from.NetState == null)
return; // No gump created at all
from.SendGump(new MyGump(from));
}
```
## Edge Cases & Gotchas
### 1. using Server.Gumps Is Required
The extension methods `SendGump()`, `HasGump<T>()`, `FindGump<T>()`, `CloseGump<T>()` are in the `Server.Gumps` namespace. Without the using, they won't resolve.
### 2. StaticGump Caching
StaticGump caches layout bytes on first use. During development, override `Cached => false` to rebuild every time, but remove before committing.
### 3. Button ID 0 = Close
Button ID 0 is reserved for close/cancel. Use 1+ for action buttons.
### 4. GumpButtonType.Reply Is Default
In ModernUO, `AddButton(x, y, n, p, id)` defaults to Reply type. Only specify `GumpButtonType.Page` for page navigation buttons.
## See Also
- `dev-docs/gump-system.md` — Complete ModernUO gump reference
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,309 @@
# Packets & Networking Migration
## Overview
RunUO uses a `Packet` class hierarchy where outgoing packets are objects with `PacketWriter`, and incoming packets use `PacketHandler` with `PacketReader`. ModernUO completely removes the `Packet` class hierarchy. Outgoing packets are static methods using `SpanWriter` with stack-allocated buffers. Incoming packets are registered function pointers using `SpanReader`.
## RunUO Outgoing Packet Pattern
```csharp
// RunUO — Packet subclass
public sealed class MyPacket : Packet
{
public MyPacket(Serial target, int value) : base(0xBF, 12)
{
m_Stream.Write((ushort)12);
m_Stream.Write((ushort)0x99);
m_Stream.Write((int)target);
m_Stream.Write((short)value);
}
}
// Usage:
ns.Send(new MyPacket(target.Serial, 42));
```
## ModernUO Outgoing Packet Pattern
```csharp
// ModernUO — Static create method + extension method
public static class OutgoingMyPackets
{
public const int MyPacketLength = 12;
public static void CreateMyPacket(Span<byte> buffer, Serial target, int value)
{
if (buffer[0] != 0)
return;
var writer = new SpanWriter(buffer);
writer.Write((byte)0xBF);
writer.Write((ushort)12);
writer.Write((ushort)0x99);
writer.Write(target);
writer.Write((short)value);
}
}
public static class MyPacketExtensions
{
public static void SendMyPacket(this NetState ns, Serial target, int value)
{
if (ns.CannotSendPackets())
return;
var buffer = stackalloc byte[OutgoingMyPackets.MyPacketLength].InitializePacket();
OutgoingMyPackets.CreateMyPacket(buffer, target, value);
ns.Send(buffer);
}
}
// Usage:
mobile.NetState.SendMyPacket(target.Serial, 42);
```
## RunUO Incoming Packet Pattern
```csharp
// RunUO — PacketHandler registration
public class MyPacketHandlers
{
public static void Initialize()
{
PacketHandlers.Register(0x99, 12, true, new OnPacketReceive(MyHandler));
}
public static void MyHandler(NetState state, PacketReader pvSrc)
{
Serial target = pvSrc.ReadInt32();
int value = pvSrc.ReadInt16();
// Process...
}
}
```
## ModernUO Incoming Packet Pattern
```csharp
// ModernUO — Function pointer registration
public static class IncomingMyPackets
{
public static unsafe void Configure()
{
IncomingPackets.Register(0x99, 12, true, &MyHandler);
}
public static void MyHandler(NetState state, SpanReader reader)
{
var target = (Serial)reader.ReadUInt32();
var value = reader.ReadInt16();
// Process...
}
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `class MyPacket : Packet` | Static `CreateXxx(Span<byte>)` method | No class hierarchy |
| `Packet(packetId, length)` constructor | `new SpanWriter(buffer)` | Stack-allocated buffer |
| `m_Stream.Write(value)` | `writer.Write(value)` | Same method names |
| `PacketWriter` | `SpanWriter` | Ref struct, stack-allocated |
| `PacketReader` | `SpanReader` | Ref struct |
| `pvSrc.ReadInt32()` | `reader.ReadInt32()` | Same names |
| `pvSrc.ReadString()` | `reader.ReadAsciiSafe()` or `reader.ReadBigUniSafe()` | Explicit encoding |
| `pvSrc.ReadUnicodeStringSafe()` | `reader.ReadBigUniSafe()` | Explicit name |
| `ns.Send(new MyPacket(...))` | `ns.SendMyPacket(...)` | Extension method |
| `PacketHandlers.Register(id, len, ingame, handler)` | `IncomingPackets.Register(id, len, ingame, &handler)` | Function pointer |
| `new OnPacketReceive(handler)` | `&handler` | Function pointer, no delegate |
| `Packet.Compile()` / `Packet.SetStatic()` | Not needed | Buffer is stack-allocated |
| `Packet.Acquire()` / `Packet.Release()` | Not needed | No pooling, stack memory |
| Variable-length: `this.EnsureCapacity(len)` | `writer.WritePacketLength()` at end | Fill length at position 1-2 |
## Step-by-Step Conversion
### Outgoing Packets
#### Step 1: Create Static Class
```csharp
public static class OutgoingMySystemPackets
{
// Constants and Create methods go here
}
```
#### Step 2: Convert Packet Constructor to Create Method
```csharp
// Determine packet length from RunUO constructor: base(0xBF, 12)
public const int MyPacketLength = 12;
public static void CreateMyPacket(Span<byte> buffer, Serial target, int value)
{
if (buffer[0] != 0) // Already initialized guard
return;
var writer = new SpanWriter(buffer);
writer.Write((byte)0xBF); // Packet ID
writer.Write((ushort)12); // Length
writer.Write((ushort)0x99); // Sub-command
writer.Write(target); // Serial
writer.Write((short)value); // Value
}
```
#### Step 3: Create Send Extension Method
```csharp
public static void SendMyPacket(this NetState ns, Serial target, int value)
{
if (ns.CannotSendPackets())
return;
var buffer = stackalloc byte[MyPacketLength].InitializePacket();
CreateMyPacket(buffer, target, value);
ns.Send(buffer);
}
```
#### Step 4: For Variable-Length Packets
```csharp
public static void SendDynamicPacket(this NetState ns, string name, int[] values)
{
if (ns.CannotSendPackets())
return;
var length = 7 + name.Length * 2 + values.Length * 4;
var writer = new SpanWriter(stackalloc byte[length]);
writer.Write((byte)0x99);
writer.Write((ushort)0); // Length placeholder
writer.WriteBigUniNull(name);
writer.Write((ushort)values.Length);
foreach (var val in values)
writer.Write(val);
writer.WritePacketLength(); // Fill in actual length
ns.Send(writer.Span);
}
```
### Incoming Packets
#### Step 1: Change Registration
```csharp
// RunUO (Initialize)
PacketHandlers.Register(0x99, 12, true, new OnPacketReceive(MyHandler));
// ModernUO (Configure, unsafe)
public static unsafe void Configure()
{
IncomingPackets.Register(0x99, 12, true, &MyHandler);
}
```
#### Step 2: Update Handler Signature
```csharp
// RunUO
public static void MyHandler(NetState state, PacketReader pvSrc)
// ModernUO
public static void MyHandler(NetState state, SpanReader reader)
```
#### Step 3: Update Read Methods
```csharp
// RunUO → ModernUO
pvSrc.ReadInt32() → reader.ReadInt32()
pvSrc.ReadInt16() → reader.ReadInt16()
pvSrc.ReadByte() → reader.ReadByte()
pvSrc.ReadBoolean() → reader.ReadBoolean()
pvSrc.ReadString() → reader.ReadAsciiSafe()
pvSrc.ReadUnicodeStringSafe() → reader.ReadBigUniSafe()
pvSrc.ReadUnicodeString() → reader.ReadBigUni()
pvSrc.Seek(offset, origin) → reader.Seek(offset, origin)
```
## SpanWriter Quick Reference
```csharp
writer.Write(bool); // 1 byte
writer.Write(byte); // 1 byte
writer.Write(short); // 2 bytes big-endian
writer.Write(ushort); // 2 bytes big-endian
writer.Write(int); // 4 bytes big-endian
writer.Write(uint); // 4 bytes big-endian
writer.Write(Serial); // 4 bytes
writer.WriteAsciiNull(str); // ASCII null-terminated
writer.WriteBigUniNull(str); // UTF-16 BE null-terminated
writer.WriteLE(int); // 4 bytes little-endian
writer.WritePacketLength(); // Fill length at pos 1-2
```
## SpanReader Quick Reference
```csharp
reader.ReadByte(); // 1 byte
reader.ReadBoolean(); // 1 byte
reader.ReadInt16(); // 2 bytes big-endian
reader.ReadUInt16(); // 2 bytes big-endian
reader.ReadInt32(); // 4 bytes big-endian
reader.ReadUInt32(); // 4 bytes big-endian
reader.ReadAsciiSafe(); // ASCII, filtered
reader.ReadBigUniSafe(); // UTF-16 BE, filtered
reader.Seek(offset, origin); // Position
reader.Remaining; // Bytes left
```
## Shared Buffer Pattern
When sending the same packet to multiple players:
```csharp
public static void SendToNearby(Mobile source, int effectId)
{
Span<byte> buffer = stackalloc byte[EffectPacketLength];
buffer.InitializePacket();
foreach (var ns in source.GetClientsInRange(18))
{
CreateEffectPacket(buffer, source.Serial, effectId);
ns.Send(buffer);
}
}
```
The `buffer[0] != 0` guard in `Create` methods prevents re-initialization, so the buffer is built once and reused.
## Edge Cases & Gotchas
### 1. Always Check CannotSendPackets()
```csharp
if (ns.CannotSendPackets())
return;
```
### 2. Use InitializePacket() on stackalloc
```csharp
var buffer = stackalloc byte[length].InitializePacket();
```
### 3. Big-Endian by Default
UO protocol is big-endian. Only use `WriteLE`/`ReadLE` when specifically needed.
### 4. Use Safe String Reads
Always use `ReadAsciiSafe()`/`ReadBigUniSafe()` for incoming strings to filter control characters.
### 5. Function Pointers Require unsafe
The `Configure()` method must be marked `unsafe` for function pointer syntax `&handler`.
### 6. Many Common Packets Already Exist
Before writing custom packet code, check if ModernUO already has a `Send*` extension method in:
- `OutgoingMobilePackets` — Mobile status, animation, movement
- `OutgoingItemPackets` — Item display, updates
- `OutgoingEffectPackets` — Effects, sounds
- `OutgoingContainerPackets` — Container contents
## See Also
- `dev-docs/networking-packets.md` — Complete ModernUO networking reference
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,194 @@
# Property Lists (Tooltips) Migration
## Overview
RunUO uses `ObjectPropertyList` as both the interface and implementation for item/mobile tooltips. ModernUO introduces an `IPropertyList` interface and has a critical rule: string literals in interpolated strings must be wrapped as holes (`{"text"}` not `text`).
## RunUO Pattern
```csharp
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString()); // "charges: ~1_val~"
list.Add(1060658, "Map\t" + m_MapDest); // "~1_val~: ~2_val~"
list.Add(1060637, "{0}\t{1}", m_Current, m_Max); // "~1_val~ / ~2_val~"
list.Add("Custom text line");
}
```
## ModernUO Equivalent
```csharp
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}"); // "charges: ~1_val~"
list.Add(1060658, $"{"Map"}\t{_mapDest}"); // "~1_val~: ~2_val~" — "Map" is a hole!
list.Add(1060637, $"{_current}\t{_max}"); // "~1_val~ / ~2_val~"
list.Add($"{"Custom text line"}"); // Raw string — still a hole
}
```
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `GetProperties(ObjectPropertyList list)` | `GetProperties(IPropertyList list)` | Interface instead of class |
| `list.Add(number, string.Format(...))` | `list.Add(number, $"...")` | Interpolated string |
| `list.Add(number, value.ToString())` | `list.Add(number, $"{value}")` | Interpolated |
| `list.Add(number, "text\t" + value)` | `list.Add(number, $"{"text"}\t{value}")` | Text must be hole |
| `list.Add(number, string.Format("{0}\t{1}", a, b))` | `list.Add(number, $"{a}\t{b}")` | Tab-separated args |
| `list.Add("raw string")` | `list.Add($"{"raw string"}")` | String literal as hole |
| `list.Add(number)` | `list.Add(number)` | Same — cliloc only |
| `list.Add(number, "#" + cliloc)` | `list.Add(number, $"{cliloc:#}")` | Cliloc as argument |
## The String Literal Rule (CRITICAL)
The `IPropertyList` interpolated string handler distinguishes between **literals** (text between `{}` holes) and **holes** (values inside `{}`). Literals are treated as delimiters (like `\t`). Holes are treated as cliloc arguments.
**Rule**: The only bare literal text should be `\t` (argument separator). All other text must be inside `{}` holes.
```csharp
// BAD — "Map" is a literal, treated as a delimiter
list.Add(1060658, $"Map\t{_mapDest}");
// GOOD — "Map" is a hole, treated as argument ~1_val~
list.Add(1060658, $"{"Map"}\t{_mapDest}");
```
Why this matters: The property list data is consumed beyond just the game client (e.g., web renderers). The system must distinguish arguments from delimiters to correctly format tooltips for all consumers.
### Cliloc Number as Argument
When a cliloc argument is itself a cliloc number to resolve:
```csharp
// BAD — string "#1060000" is not properly handled by all consumers
list.Add(1050039, $"{_amount}\t{"#1060000"}");
// GOOD — :# format specifier marks it as a cliloc reference
list.Add(1050039, $"{_amount}\t{1060000:#}");
```
Or use the convenience methods:
```csharp
list.AddLocalized(clilocNumber); // Single cliloc value
list.AddLocalized(1050039, clilocNumber); // Cliloc with cliloc argument
```
## Step-by-Step Conversion
### Step 1: Change Method Signature
```csharp
// RunUO
public override void GetProperties(ObjectPropertyList list)
// ModernUO
public override void GetProperties(IPropertyList list)
```
### Step 2: Always Call Base First
```csharp
base.GetProperties(list); // Unchanged
```
### Step 3: Convert Each list.Add() Call
**Simple value:**
```csharp
// RunUO
list.Add(1060741, m_Charges.ToString());
// ModernUO
list.Add(1060741, $"{_charges}");
```
**Multiple tab-separated arguments:**
```csharp
// RunUO
list.Add(1060637, string.Format("{0}\t{1}", m_Current, m_Max));
// ModernUO
list.Add(1060637, $"{_current}\t{_max}");
```
**String literal arguments:**
```csharp
// RunUO
list.Add(1060658, "Coords\t" + m_Location.ToString());
// ModernUO
list.Add(1060658, $"{"Coords"}\t{_location}");
```
**Raw text:**
```csharp
// RunUO
list.Add("Soulbound");
// ModernUO
list.Add($"{"Soulbound"}");
```
### Step 4: Add [InvalidateProperties] to Serialized Fields
If the RunUO property setter called `InvalidateProperties()`, add the attribute:
```csharp
[SerializableField(0)]
[InvalidateProperties] // Auto-refreshes tooltip
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
```
## Before/After Example
**RunUO:**
```csharp
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString());
list.Add(1060658, "Map\t" + m_MapDest);
list.Add(1060659, "Coords\t" + m_PointDest);
list.Add(1060660, "Creatures\t" + (m_Creatures ? "Yes" : "No"));
list.Add(1060661, "Range\t" + m_Range);
if (m_Active)
list.Add(1060742); // "active"
}
```
**ModernUO:**
```csharp
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}");
list.Add(1060658, $"{"Map"}\t{_mapDest}");
list.Add(1060659, $"{"Coords"}\t{_pointDest}");
list.Add(1060660, $"{"Creatures"}\t{(Creatures ? "Yes" : "No")}");
list.Add(1060661, $"{"Range"}\t{_range}");
if (_active)
list.Add(1060742);
}
```
## Edge Cases & Gotchas
### 1. Integer Overload vs String Interpolation
`list.Add(number, int)` exists and is different from `list.Add(number, $"{int}")`. The integer overload passes the raw int; the interpolation formats it as a string. Use whichever matches the cliloc expectation.
### 2. Era-Conditional Properties
Check era when properties differ between expansions:
```csharp
if (Core.ML)
list.Add(1072241, $"{TotalItems}\t{MaxItems}\t{TotalWeight}\t{MaxWeight}");
else
list.Add(1050044, $"{TotalItems}\t{TotalWeight}");
```
### 3. Don't Call InvalidateProperties() in Loops
It triggers hash computation and potential network sends. Batch changes first.
## See Also
- `dev-docs/property-lists.md` — Complete ModernUO property list reference
- `02-serialization.md` — [InvalidateProperties] on serialized fields
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,211 @@
# Commands & Events Migration
## Overview
Commands are largely similar between RunUO and ModernUO — the `CommandSystem.Register()` API is the same. The main changes are handler attribute conventions and that events now use `Action<T>` delegates instead of custom delegate types.
## Command Changes
### Registration (Same Pattern)
```csharp
// Both RunUO and ModernUO
public static void Configure() // ModernUO uses Configure(), RunUO may use Initialize()
{
CommandSystem.Register("MyCommand", AccessLevel.GameMaster, MyCommand_OnCommand);
}
```
**Key difference**: ModernUO prefers `Configure()` for registration. RunUO often uses `Initialize()`. Both work, but `Configure()` runs earlier in the startup sequence and is the convention.
### Handler Attributes
```csharp
// RunUO
[Usage("MyCommand <arg>")]
[Description("Does something")]
// ModernUO — same attributes, plus optional Aliases
[Usage("MyCommand <arg>")]
[Description("Does something")]
[Aliases("mc", "mycmd")]
```
### CommandEventArgs (Same)
```csharp
public static void MyCommand_OnCommand(CommandEventArgs e)
{
var from = e.Mobile;
var name = e.GetString(0);
var count = e.Length > 1 ? e.GetInt32(1) : 1;
}
```
No changes needed for command handlers themselves.
## Event System Migration
### RunUO EventSink Pattern
```csharp
// RunUO — custom delegate types
public static void Initialize()
{
EventSink.WorldSave += new WorldSaveEventHandler(OnWorldSave);
EventSink.WorldLoad += new WorldLoadEventHandler(OnWorldLoad);
EventSink.Login += new LoginEventHandler(OnLogin);
EventSink.Logout += new LogoutEventHandler(OnLogout);
EventSink.Speech += new SpeechEventHandler(OnSpeech);
EventSink.Movement += new MovementEventHandler(OnMovement);
EventSink.ServerStarted += new ServerStartedEventHandler(OnServerStarted);
EventSink.Crashed += new CrashedEventHandler(OnCrashed);
}
private static void OnWorldSave(WorldSaveEventArgs e)
{
// Save data to file
}
private static void OnLogin(LoginEventArgs e)
{
Mobile m = e.Mobile;
m.SendMessage("Welcome!");
}
```
### ModernUO EventSink Pattern
```csharp
// ModernUO — Action<T> delegates, changed event names
public static void Configure()
{
EventSink.WorldSave += OnWorldSave; // Action (no args)
EventSink.WorldLoad += OnWorldLoad; // Action (no args)
EventSink.Connected += OnConnected; // Action<Mobile> — was Login
EventSink.Disconnected += OnDisconnected; // Action<Mobile> — was Logout
EventSink.Speech += OnSpeech; // Action<SpeechEventArgs>
EventSink.Movement += OnMovement; // Action<MovementEventArgs>
EventSink.ServerStarted += OnServerStarted; // Action (no args)
EventSink.ServerCrashed += OnCrashed; // Action<ServerCrashedEventArgs>
}
private static void OnWorldSave()
{
// For persistence, use GenericPersistence instead (see 08-persistence.md)
}
private static void OnConnected(Mobile m)
{
m.SendMessage("Welcome!");
}
```
## Event Migration Mapping
| RunUO Event | ModernUO Event | Signature Change |
|---|---|---|
| `EventSink.WorldSave` | `EventSink.WorldSave` | `WorldSaveEventArgs``Action` (no args) |
| `EventSink.WorldLoad` | `EventSink.WorldLoad` | `WorldLoadEventArgs``Action` (no args) |
| `EventSink.Login` | `EventSink.Connected` | `LoginEventArgs``Action<Mobile>` |
| `EventSink.Logout` | `EventSink.Disconnected` | `LogoutEventArgs``Action<Mobile>` |
| `EventSink.Speech` | `EventSink.Speech` | `SpeechEventArgs` (mostly same) |
| `EventSink.Movement` | `EventSink.Movement` | `MovementEventArgs` (mostly same) |
| `EventSink.ServerStarted` | `EventSink.ServerStarted` | `Action` (no args) |
| `EventSink.Crashed` | `EventSink.ServerCrashed` | Renamed |
| `EventSink.AggressiveAction` | `EventSink.AggressiveAction` | `AggressiveActionEventArgs` |
| `EventSink.AccountLogin` | `EventSink.AccountLogin` | `AccountLoginEventArgs` |
| `EventSink.SocketConnect` | `EventSink.SocketConnect` | `SocketConnectEventArgs` |
| `EventSink.BeforeWorldSave` | Removed | Use `WorldSave` event directly |
| `EventSink.Shutdown` | `EventSink.Shutdown` | `Action` |
| `EventSink.CharacterCreated` | Removed/Restructured | Check current source |
| `EventSink.OpenDoorMacroUsed` | Removed | Handle in movement/speech |
| `EventSink.PlayerDeath` | Use CodeGeneratedEvents | `PlayerMobile.PlayerDeathEvent` |
| `EventSink.CreatureDeath` | Use CodeGeneratedEvents | `BaseCreature.CreatureDeathEvent` |
## Step-by-Step Conversion
### Step 1: Change Initialize() to Configure()
```csharp
// RunUO
public static void Initialize()
// ModernUO
public static void Configure()
```
### Step 2: Remove Delegate Type Constructors
```csharp
// RunUO
EventSink.Login += new LoginEventHandler(OnLogin);
// ModernUO
EventSink.Connected += OnConnected;
```
### Step 3: Update Event Names
Rename `Login` to `Connected`, `Logout` to `Disconnected`, etc. (see mapping table).
### Step 4: Update Handler Signatures
```csharp
// RunUO
private static void OnLogin(LoginEventArgs e)
{
Mobile m = e.Mobile;
}
// ModernUO
private static void OnConnected(Mobile m)
{
// Mobile is passed directly
}
```
### Step 5: WorldSave/WorldLoad → GenericPersistence
If the event handler was saving/loading data to binary files, convert to `GenericPersistence` instead. See `08-persistence.md`.
```csharp
// RunUO — manual file persistence via EventSink
EventSink.WorldSave += new WorldSaveEventHandler(Save);
EventSink.WorldLoad += new WorldLoadEventHandler(Load);
// ModernUO — use GenericPersistence class (see 08-persistence.md)
// Don't use EventSink.WorldSave for custom persistence
```
## CodeGeneratedEvents
For entity-specific events, ModernUO uses source-generated events:
```csharp
// Subscribing to a generated event
[OnEvent(nameof(PlayerMobile.PlayerLoginEvent))]
public static void HandlePlayerLogin(PlayerMobile player)
{
// Handle player login
}
```
Known generated events:
- `PlayerMobile.PlayerLoginEvent`
- `PlayerMobile.PlayerDeathEvent`
- `BaseCreature.CreatureDeathEvent`
## Edge Cases & Gotchas
### 1. WorldSave No Longer Has EventArgs
RunUO's `WorldSaveEventArgs` contained the save path. In ModernUO, use `WorldSavePostSnapshot` event if you need paths, or better yet, use `GenericPersistence`.
### 2. Login/Logout → Connected/Disconnected
The names changed AND the signatures changed. `LoginEventArgs.Mobile` → direct `Mobile` parameter.
### 3. Subscribe in Configure(), Not Initialize()
`Configure()` runs before `Initialize()`. Event subscriptions should happen early.
### 4. Pooled EventArgs
Some events (Movement, AggressiveAction) use pooled args. Don't store references to them — they get recycled.
### 5. Handled/Blocked Properties
`SpeechEventArgs.Handled` and `.Blocked` work the same. Set `Handled = true` to consume, `Blocked = true` to block.
## See Also
- `dev-docs/events.md` — Complete ModernUO event system reference
- `dev-docs/commands-targeting.md` — Complete command/targeting reference
- `08-persistence.md` — Converting WorldSave persistence
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,398 @@
# Persistence Migration
## Overview
RunUO systems that persist custom data use `EventSink.WorldSave`/`EventSink.WorldLoad` with manual `BinaryFileWriter`/`BinaryFileReader`. ModernUO replaces this with `GenericPersistence` (for system state) and `GenericEntityPersistence<T>` (for custom entity collections). The persistence framework handles save/load lifecycle, file management, and integration with ModernUO's multi-threaded save pipeline automatically.
## RunUO Pattern
```csharp
using System;
using System.IO;
using Server;
namespace Server.Custom
{
public class JailSystem
{
private static Dictionary<Mobile, JailRecord> m_Records = new Dictionary<Mobile, JailRecord>();
public static void Configure()
{
EventSink.WorldLoad += new WorldLoadEventHandler(Load);
EventSink.WorldSave += new WorldSaveEventHandler(Save);
}
private static void Load()
{
string filePath = Path.Combine("Saves/Custom", "JailSystem.bin");
if (!File.Exists(filePath))
return;
using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
BinaryReader reader = new BinaryReader(fs);
int version = reader.ReadInt32();
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
Mobile m = World.FindMobile(reader.ReadInt32());
string reason = reader.ReadString();
DateTime releaseDate = new DateTime(reader.ReadInt64());
if (m != null)
m_Records[m] = new JailRecord(reason, releaseDate);
}
}
}
private static void Save(WorldSaveEventArgs e)
{
string dirPath = "Saves/Custom";
if (!Directory.Exists(dirPath))
Directory.CreateDirectory(dirPath);
string filePath = Path.Combine(dirPath, "JailSystem.bin");
using (FileStream fs = new FileStream(filePath, FileMode.Create, FileAccess.Write))
{
BinaryWriter writer = new BinaryWriter(fs);
writer.Write((int)0); // version
writer.Write(m_Records.Count);
foreach (var kvp in m_Records)
{
writer.Write(kvp.Key.Serial.Value);
writer.Write(kvp.Value.Reason);
writer.Write(kvp.Value.ReleaseDate.Ticks);
}
}
}
}
}
```
## ModernUO Equivalent (GenericPersistence)
```csharp
using Server;
using Server.Serialization;
namespace Server.Custom;
public class JailSystem : GenericPersistence
{
private static JailSystem _instance;
private static Dictionary<Mobile, JailRecord> _records = new();
public static void Configure()
{
_instance = new JailSystem();
}
public JailSystem() : base("JailSystem", 10) { }
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_records.Count);
foreach (var (mobile, record) in _records)
{
writer.Write(mobile);
writer.Write(record.Reason);
writer.Write(record.ReleaseDate);
}
}
public override void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
for (var i = 0; i < count; i++)
{
var mobile = reader.ReadEntity<Mobile>();
var reason = reader.ReadString();
var releaseDate = reader.ReadDateTime();
if (mobile != null)
_records[mobile] = new JailRecord(reason, releaseDate);
}
}
// Public API for the system
public static void JailPlayer(Mobile m, string reason, DateTime releaseDate)
{
_records[m] = new JailRecord(reason, releaseDate);
_instance.MarkDirty();
}
public static bool IsJailed(Mobile m) => _records.ContainsKey(m);
public static void Release(Mobile m)
{
_records.Remove(m);
_instance.MarkDirty();
}
}
```
## When to Use Which
| Scenario | Use |
|---|---|
| System state (jail records, virtue data, faction data, scores) | `GenericPersistence` |
| Custom entity collections with their own serial ranges | `GenericEntityPersistence<T>` |
| Item/Mobile subclasses (normal serialization) | `[SerializationGenerator]` — not persistence |
Most RunUO `EventSink.WorldSave` patterns should convert to `GenericPersistence`.
## Migration Mapping Table
| RunUO | ModernUO | Notes |
|---|---|---|
| `EventSink.WorldSave += Save` | `class MySystem : GenericPersistence` | Subclass instead |
| `EventSink.WorldLoad += Load` | `override Deserialize(IGenericReader)` | Method on class |
| Manual `Save(WorldSaveEventArgs)` | `override Serialize(IGenericWriter)` | Method on class |
| Manual `Load()` | `override Deserialize(IGenericReader)` | Method on class |
| `new BinaryFileWriter(path, true)` | Handled by framework | No file management |
| `new BinaryFileReader(new FileStream(...))` | Handled by framework | No file management |
| `writer.Write((int)0)` version | `writer.WriteEncodedInt(0)` | Encoded preferred |
| `reader.ReadInt()` count | `reader.ReadEncodedInt()` | Encoded preferred |
| `writer.Write(mobile.Serial.Value)` | `writer.Write(mobile)` | Write entity directly |
| `World.FindMobile(reader.ReadInt32())` | `reader.ReadEntity<Mobile>()` | Generic method |
| `World.FindItem(reader.ReadInt32())` | `reader.ReadEntity<Item>()` | Generic method |
| `Directory.CreateDirectory(...)` | Handled by framework | Automatic |
| `File.Exists(path)` check | Handled by framework | Automatic |
## Step-by-Step Conversion
### Step 1: Create Persistence Class
```csharp
public class MySystem : GenericPersistence
{
private static MySystem _instance;
public static void Configure()
{
_instance = new MySystem();
}
public MySystem() : base("MySystem", 10) { }
// "MySystem" = save file name
// 10 = priority (lower = saved first)
}
```
### Step 2: Move Save Logic to Serialize
```csharp
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
// Convert BinaryWriter calls to IGenericWriter calls
writer.WriteEncodedInt(_data.Count);
foreach (var (key, value) in _data)
{
writer.Write(key); // Can write Mobile/Item directly
writer.Write(value);
}
}
```
### Step 3: Move Load Logic to Deserialize
```csharp
public override void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
for (var i = 0; i < count; i++)
{
var key = reader.ReadEntity<Mobile>(); // Not World.FindMobile()
var value = reader.ReadInt();
if (key != null)
_data[key] = value;
}
}
```
### Step 4: Remove EventSink Subscriptions
Delete the `EventSink.WorldSave += ...` and `EventSink.WorldLoad += ...` lines.
### Step 5: Add MarkDirty() Calls
Whenever data changes, call `_instance.MarkDirty()` to flag the system for saving:
```csharp
public static void AddRecord(Mobile m, string data)
{
_records[m] = data;
_instance.MarkDirty(); // Required!
}
```
### Step 6: Remove File Management Code
Delete all `Directory.CreateDirectory`, `File.Exists`, `FileStream`, path construction. The framework handles this.
## IGenericWriter vs BinaryWriter
| BinaryWriter (RunUO) | IGenericWriter (ModernUO) |
|---|---|
| `writer.Write((int)value)` | `writer.Write(value)` or `writer.WriteEncodedInt(value)` |
| `writer.Write((string)value)` | `writer.Write(value)` |
| `writer.Write((bool)value)` | `writer.Write(value)` |
| `writer.Write(mobile.Serial.Value)` | `writer.Write(mobile)` |
| `writer.Write(item.Serial.Value)` | `writer.Write(item)` |
| `writer.Write(dateTime.Ticks)` | `writer.Write(dateTime)` |
| No encoded int | `writer.WriteEncodedInt(value)` — variable-length, saves space |
## IGenericReader vs BinaryReader
| BinaryReader (RunUO) | IGenericReader (ModernUO) |
|---|---|
| `reader.ReadInt32()` | `reader.ReadInt()` or `reader.ReadEncodedInt()` |
| `reader.ReadString()` | `reader.ReadString()` |
| `reader.ReadBoolean()` | `reader.ReadBool()` |
| `World.FindMobile(reader.ReadInt32())` | `reader.ReadEntity<Mobile>()` |
| `World.FindItem(reader.ReadInt32())` | `reader.ReadEntity<Item>()` |
| `new DateTime(reader.ReadInt64())` | `reader.ReadDateTime()` |
## Before/After: Complete System
**RunUO:**
```csharp
namespace Server.Custom
{
public class VirtueSystem
{
private static Dictionary<Mobile, int> m_Points = new Dictionary<Mobile, int>();
public static void Configure()
{
EventSink.WorldLoad += new WorldLoadEventHandler(Load);
EventSink.WorldSave += new WorldSaveEventHandler(Save);
}
private static void Load()
{
string path = Path.Combine("Saves/Custom", "Virtue.bin");
if (!File.Exists(path)) return;
using var fs = new FileStream(path, FileMode.Open);
var reader = new BinaryReader(fs);
int version = reader.ReadInt32();
int count = reader.ReadInt32();
for (int i = 0; i < count; i++)
{
Mobile m = World.FindMobile(reader.ReadInt32());
int pts = reader.ReadInt32();
if (m != null)
m_Points[m] = pts;
}
}
private static void Save(WorldSaveEventArgs e)
{
string dir = "Saves/Custom";
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
using var fs = new FileStream(Path.Combine(dir, "Virtue.bin"), FileMode.Create);
var writer = new BinaryWriter(fs);
writer.Write(0); // version
writer.Write(m_Points.Count);
foreach (var kvp in m_Points)
{
writer.Write(kvp.Key.Serial.Value);
writer.Write(kvp.Value);
}
}
public static void AddPoints(Mobile m, int points)
{
if (!m_Points.ContainsKey(m))
m_Points[m] = 0;
m_Points[m] += points;
}
}
}
```
**ModernUO:**
```csharp
namespace Server.Custom;
public class VirtueSystem : GenericPersistence
{
private static VirtueSystem _instance;
private static readonly Dictionary<Mobile, int> _points = new();
public static void Configure()
{
_instance = new VirtueSystem();
}
public VirtueSystem() : base("VirtueSystem", 10) { }
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0); // version
writer.WriteEncodedInt(_points.Count);
foreach (var (mobile, pts) in _points)
{
writer.Write(mobile);
writer.Write(pts);
}
}
public override void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
for (var i = 0; i < count; i++)
{
var mobile = reader.ReadEntity<Mobile>();
var pts = reader.ReadInt();
if (mobile != null)
_points[mobile] = pts;
}
}
public static void AddPoints(Mobile m, int points)
{
_points.TryGetValue(m, out var current);
_points[m] = current + points;
_instance.MarkDirty();
}
}
```
## Edge Cases & Gotchas
### 1. MarkDirty() Is Required
Without `MarkDirty()`, changes won't be saved. Call it whenever your persisted data changes.
### 2. GenericPersistence Constructor Name
The first argument to the base constructor is the save file name. It must be unique across all `GenericPersistence` instances.
### 3. Priority Argument
The second argument is save priority. Lower numbers save first. Use 10 for most systems.
### 4. Don't Mix EventSink.WorldSave with GenericPersistence
Don't subscribe to `EventSink.WorldSave` for data that `GenericPersistence` manages. The framework handles the lifecycle.
### 5. ReadEntity<T>() Returns Null for Deleted Entities
Unlike `World.FindMobile()`, `ReadEntity<T>()` will return null if the entity was deleted. Always null-check.
## See Also
- `dev-docs/serialization.md` — Serialization system overview
- `07-commands-events.md` — EventSink migration
- `01-foundation-changes.md` — Foundation changes

View file

@ -0,0 +1,566 @@
# Items, Mobiles & Creatures Migration
## Overview
Most RunUO migration work involves converting Item, Mobile, and BaseCreature subclasses. This doc combines all prior system changes (serialization, timers, property lists, naming) into complete step-by-step conversion guides for the most common content types.
## Item Migration Step-by-Step
### 1. Apply Foundation Changes
- File-scoped namespace
- `using ModernUO.Serialization;`
- Rename `m_` fields to `_camelCase`
- `[Constructable]``[Constructible]`
- Replace `Console.WriteLine` with logging
- Replace `DateTime.UtcNow` with `Core.Now`
### 2. Add Serialization Attributes
```csharp
[SerializationGenerator(0, false)] // Version 0, Item subclass
public partial class MyItem : Item // Add partial
```
### 3. Convert Fields to [SerializableField]
```csharp
// RunUO
private int m_Charges;
[CommandProperty(AccessLevel.GameMaster)]
public int Charges { get { return m_Charges; } set { m_Charges = value; InvalidateProperties(); } }
// ModernUO
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
// Property auto-generated with InvalidateProperties
```
### 4. Delete Boilerplate
- Delete `public MyItem(Serial serial) : base(serial) { }`
- Delete `public override void Serialize(GenericWriter writer) { ... }`
- Delete `public override void Deserialize(GenericReader reader) { ... }`
### 5. Convert Timer Fields
```csharp
// RunUO
private InternalTimer m_Timer;
// + nested Timer class
// ModernUO
private TimerExecutionToken _timerToken;
// + direct Timer.StartTimer() calls
// + [AfterDeserialization] for timer restoration
// + OnAfterDelete() for timer cancellation
```
### 6. Convert GetProperties
```csharp
// RunUO
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString());
}
// ModernUO
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}");
}
```
### 7. Convert Context Menus (if present)
```csharp
// RunUO
public override void GetContextMenuEntries(Mobile from, List<ContextMenuEntry> list)
{
base.GetContextMenuEntries(from, list);
list.Add(new MyEntry(this));
}
// ModernUO
public override void GetContextMenuEntries(Mobile from, ref PooledRefList<ContextMenuEntry> list)
{
base.GetContextMenuEntries(from, ref list);
list.Add(new MyEntry(this));
}
```
## Complete Before/After: Item
**RunUO:**
```csharp
using System;
using Server;
using Server.Network;
namespace Server.Items
{
public class MagicLantern : Item
{
private int m_Charges;
private Mobile m_Owner;
private InternalTimer m_Timer;
[CommandProperty(AccessLevel.GameMaster)]
public int Charges
{
get { return m_Charges; }
set { m_Charges = value; InvalidateProperties(); }
}
[CommandProperty(AccessLevel.GameMaster)]
public Mobile Owner
{
get { return m_Owner; }
set { m_Owner = value; }
}
[Constructable]
public MagicLantern() : base(0xA25)
{
m_Charges = Utility.RandomMinMax(5, 15);
Weight = 2.0;
Light = LightType.Circle300;
Name = "a magic lantern";
m_Timer = new InternalTimer(this);
m_Timer.Start();
}
public MagicLantern(Serial serial) : base(serial) { }
public override void OnDelete()
{
if (m_Timer != null)
m_Timer.Stop();
base.OnDelete();
}
public override void GetProperties(ObjectPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, m_Charges.ToString());
}
public override void OnDoubleClick(Mobile from)
{
if (!IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(1042001);
return;
}
if (m_Charges <= 0)
{
from.SendMessage("The lantern is depleted.");
return;
}
m_Charges--;
InvalidateProperties();
from.SendMessage("The lantern flares brightly!");
}
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)1); // version
writer.Write(m_Owner);
writer.Write(m_Charges);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
switch (version)
{
case 1:
m_Owner = reader.ReadMobile();
goto case 0;
case 0:
m_Charges = reader.ReadInt();
break;
}
m_Timer = new InternalTimer(this);
m_Timer.Start();
}
private void Glow()
{
if (m_Charges > 0)
Effects.SendLocationParticles(this, 0x376A, 9, 10, 5042);
}
private class InternalTimer : Timer
{
private MagicLantern m_Lantern;
public InternalTimer(MagicLantern lantern) : base(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3))
{
m_Lantern = lantern;
Priority = TimerPriority.OneSecond;
}
protected override void OnTick()
{
m_Lantern.Glow();
}
}
}
}
```
**ModernUO:**
```csharp
using ModernUO.Serialization;
namespace Server.Items;
[SerializationGenerator(0, false)]
public partial class MagicLantern : Item
{
[SerializableField(0)]
[InvalidateProperties]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private int _charges;
[SerializableField(1)]
[SerializedCommandProperty(AccessLevel.GameMaster)]
private Mobile _owner;
private TimerExecutionToken _glowTimer;
[Constructible]
public MagicLantern() : base(0xA25)
{
_charges = Utility.RandomMinMax(5, 15);
Weight = 2.0;
Light = LightType.Circle300;
StartGlow();
}
public override string DefaultName => "a magic lantern";
private void StartGlow()
{
Timer.StartTimer(TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(3), Glow, out _glowTimer);
}
[AfterDeserialization]
private void AfterDeserialization() => StartGlow();
public override void OnAfterDelete()
{
_glowTimer.Cancel();
base.OnAfterDelete();
}
public override void GetProperties(IPropertyList list)
{
base.GetProperties(list);
list.Add(1060741, $"{_charges}");
}
public override void OnDoubleClick(Mobile from)
{
if (!IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(1042001);
return;
}
if (_charges <= 0)
{
from.SendMessage("The lantern is depleted.");
return;
}
Charges--;
from.SendMessage("The lantern flares brightly!");
}
private void Glow()
{
if (_charges > 0)
Effects.SendLocationParticles(this, 0x376A, 9, 10, 5042);
}
}
```
**What changed:**
- File-scoped namespace
- `partial class` + `[SerializationGenerator(0, false)]`
- `[Constructable]``[Constructible]`
- `m_Charges`/`m_Owner``_charges`/`_owner` with `[SerializableField]`
- Manual properties → auto-generated with `[SerializedCommandProperty]`
- `InvalidateProperties()` in setter → `[InvalidateProperties]` attribute
- `Name = "..."``DefaultName =>` property override
- Serial constructor deleted
- Serialize/Deserialize deleted
- Nested InternalTimer class → `Timer.StartTimer()` + `TimerExecutionToken`
- Timer in Deserialize → `[AfterDeserialization]`
- `OnDelete()` timer stop → `OnAfterDelete()` + `_token.Cancel()`
- `ObjectPropertyList``IPropertyList`
- `GetProperties` uses string interpolation with holes
## BaseCreature Migration
BaseCreature subclasses follow the same pattern as items but have additional considerations.
### Key Differences from Items
1. Constructor calls `base(AIType, FightMode)` instead of `base(itemID)`
2. Stats set with `SetStr()`, `SetDex()`, `SetInt()`, etc.
3. Damage/resistance types set explicitly
4. `GenerateLoot()` override for loot tables
5. Many property overrides (CorpseName, Meat, Hides, etc.)
### Before/After: Simple Creature
**RunUO:**
```csharp
namespace Server.Mobiles
{
[CorpseName("a wolf corpse")]
public class ForestWolf : BaseCreature
{
[Constructable]
public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest, 10, 1, 0.2, 0.4)
{
Name = "a forest wolf";
Body = 225;
BaseSoundID = 0xE5;
SetStr(80, 120);
SetDex(90, 110);
SetInt(20, 40);
SetHits(60, 80);
SetDamage(8, 14);
SetDamageType(ResistanceType.Physical, 100);
SetResistance(ResistanceType.Physical, 25, 35);
SetSkill(SkillName.MagicResist, 30.0, 50.0);
SetSkill(SkillName.Tactics, 50.0, 70.0);
SetSkill(SkillName.Wrestling, 50.0, 70.0);
Fame = 600;
Karma = 0;
VirtualArmor = 28;
Tamable = true;
ControlSlots = 1;
MinTameSkill = 50.1;
}
public ForestWolf(Serial serial) : base(serial) { }
public override int Meat { get { return 1; } }
public override int Hides { get { return 6; } }
public override FoodType FavoriteFood { get { return FoodType.Meat; } }
public override PackInstinct PackInstinct { get { return PackInstinct.Canine; } }
public override void GenerateLoot()
{
AddLoot(LootPack.Meager);
}
public override void Serialize(GenericWriter writer)
{
base.Serialize(writer);
writer.Write((int)0);
}
public override void Deserialize(GenericReader reader)
{
base.Deserialize(reader);
int version = reader.ReadInt();
}
}
}
```
**ModernUO:**
```csharp
using ModernUO.Serialization;
namespace Server.Mobiles;
[SerializationGenerator(0, false)]
public partial class ForestWolf : BaseCreature
{
[Constructible]
public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest)
{
Body = 225;
BaseSoundID = 0xE5;
SetStr(80, 120);
SetDex(90, 110);
SetInt(20, 40);
SetHits(60, 80);
SetDamage(8, 14);
SetDamageType(ResistanceType.Physical, 100);
SetResistance(ResistanceType.Physical, 25, 35);
SetSkill(SkillName.MagicResist, 30.0, 50.0);
SetSkill(SkillName.Tactics, 50.0, 70.0);
SetSkill(SkillName.Wrestling, 50.0, 70.0);
Fame = 600;
Karma = 0;
VirtualArmor = 28;
Tamable = true;
ControlSlots = 1;
MinTameSkill = 50.1;
}
public override string CorpseName => "a wolf corpse";
public override string DefaultName => "a forest wolf";
public override int Meat => 1;
public override int Hides => 6;
public override FoodType FavoriteFood => FoodType.Meat;
public override PackInstinct PackInstinct => PackInstinct.Canine;
public override void GenerateLoot()
{
AddLoot(LootPack.Meager);
}
}
```
**What changed:**
- `[CorpseName("...")]` attribute → `CorpseName` property override
- `Name = "..."``DefaultName` property override
- `BaseCreature(AI, Fight, 10, 1, 0.2, 0.4)``BaseCreature(AI, Fight)` (extra params have defaults)
- Expression-bodied property overrides
- Serialization boilerplate removed
- Serial constructor removed
## Key Creature Constructor Differences
```csharp
// RunUO — many parameters
public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest, 10, 1, 0.2, 0.4)
// 10 = RangePerception, 1 = RangeFight, 0.2 = ActiveSpeed, 0.4 = PassiveSpeed
// ModernUO — simplified (defaults built in)
public ForestWolf() : base(AIType.AI_Melee, FightMode.Closest)
```
The extra parameters (RangePerception, RangeFight, ActiveSpeed, PassiveSpeed) have sensible defaults. Only specify them if they differ from defaults.
## Common Creature Attribute Changes
| RunUO | ModernUO |
|---|---|
| `[CorpseName("a corpse")]` attribute | `public override string CorpseName => "a corpse";` |
| `Name = "a creature"` in constructor | `public override string DefaultName => "a creature";` |
| `get { return value; }` | `=> value;` expression-bodied |
## Item Name Changes
```csharp
// RunUO
Name = "a magic gem"; // Set in constructor
// ModernUO — prefer property overrides
public override string DefaultName => "a magic gem";
// OR for cliloc:
public override int LabelNumber => 1234567;
```
## Equipment/Weapon/Armor Migration
Weapons and armor follow the same item pattern but inherit from specialized base classes:
```csharp
// ModernUO weapon example
[SerializationGenerator(0, false)]
public partial class MySpecialSword : BaseSword
{
[Constructible]
public MySpecialSword() : base(0x13FF) // Katana graphic
{
Weight = 6.0;
Layer = Layer.TwoHanded;
}
public override string DefaultName => "a special sword";
public override int AosStrengthReq => 25;
public override int AosMinDamage => 11;
public override int AosMaxDamage => 13;
public override int AosSpeed => 44;
public override float MlSpeed => 2.50f;
}
```
## Common Base Classes
| RunUO | ModernUO | Notes |
|---|---|---|
| `BaseWeapon` | `BaseWeapon` | Same, add `partial` |
| `BaseSword` / `BaseMace` / etc. | Same | Same, add `partial` |
| `BaseArmor` | `BaseArmor` | Same, add `partial` |
| `BaseClothing` | `BaseClothing` | Same, add `partial` |
| `BaseJewel` | `BaseJewel` | Same, add `partial` |
| `BaseContainer` | `BaseContainer` | Same, add `partial` |
| `Food` | `Food` | Same, add `partial` |
| `BasePotion` | `BasePotion` | Same, add `partial` |
## Edge Cases & Gotchas
### 1. [TypeAlias] for Save Compatibility
If a class changed namespace or name, use `[TypeAlias]`:
```csharp
[TypeAlias("Server.Items.OldName")]
[SerializationGenerator(0, false)]
public partial class NewName : Item { }
```
### 2. OnDoubleClick Validation
ModernUO patterns prefer:
```csharp
if (!IsChildOf(from.Backpack))
{
from.SendLocalizedMessage(1042001);
return;
}
```
### 3. CorpseName as Property Override
RunUO uses `[CorpseName]` attribute. ModernUO uses a property override instead.
### 4. SetMana(0) for Non-Casters
Always call `SetMana(0)` for creatures that shouldn't have mana.
### 5. Decrement via Generated Property
Use the generated property name (PascalCase) when decrementing to trigger dirty tracking:
```csharp
Charges--; // Uses generated property — triggers MarkDirty + InvalidateProperties
// NOT: _charges--; // Bypasses tracking
```
## See Also
- `dev-docs/content-patterns.md` — ModernUO content creation patterns
- `dev-docs/serialization.md` — Serialization system
- `02-serialization.md` — Serialization migration details
- `03-timers.md` — Timer migration
- `06-property-lists.md` — Property list migration

View file

@ -0,0 +1,354 @@
# Systems & Engines Migration
## Overview
RunUO community scripts often include multi-file systems: custom crafting extensions, spawner systems, economy engines, housing addons, quest systems. Migrating these requires applying all prior changes (serialization, timers, events, persistence) systematically across multiple interdependent files.
## Approach
### 1. Map the System
Before changing code, understand the system's structure:
- List all files in the system
- Identify entry points (Configure/Initialize methods)
- Map class dependencies (what references what)
- Identify serialized types (anything with Serialize/Deserialize)
- Identify persistence (EventSink.WorldSave handlers)
- Identify timers (Timer subclasses, DelayCall)
- Identify gumps (Gump subclasses)
- Identify packets (Packet subclasses)
### 2. Conversion Order
Convert files in dependency order:
1. **Data types / enums** — No serialization changes needed, just naming/namespace
2. **Persistence classes** — Convert EventSink.WorldSave to GenericPersistence
3. **Core entities (Items/Mobiles)** — Serialization, timers, properties
4. **Gumps** — Convert to DynamicGump/StaticGump
5. **Commands** — Usually minimal changes
6. **Packets** — Convert to SpanWriter if custom packets exist
7. **Entry point / registration** — Update Configure/Initialize
### 3. File Organization
RunUO scripts are in `Scripts/` with arbitrary structure. ModernUO expects files in `Projects/UOContent/`:
| RunUO Location | ModernUO Location |
|---|---|
| `Scripts/Custom/MySystem/` | `Projects/UOContent/Engines/MySystem/` or `Projects/UOContent/Systems/MySystem/` |
| `Scripts/Items/MyItem.cs` | `Projects/UOContent/Items/{Category}/MyItem.cs` |
| `Scripts/Mobiles/MyMobile.cs` | `Projects/UOContent/Mobiles/{Category}/MyMobile.cs` |
| `Scripts/Gumps/MyGump.cs` | `Projects/UOContent/Gumps/MyGump.cs` |
| `Scripts/Commands/MyCommand.cs` | Keep with parent system or `Projects/UOContent/Commands/` |
## Pattern: Multi-File System Migration
### Example: A Custom Bounty System
**RunUO structure:**
```
Scripts/Custom/BountySystem/
├── BountySystem.cs (EventSink.WorldSave/Load, core logic)
├── BountyBoard.cs (Item - the board in town)
├── BountyBoardGump.cs (Gump - UI)
├── BountyEntry.cs (Data class - not serialized as entity)
├── BountyCommands.cs (Commands)
└── BountyTimer.cs (Timer for bounty expiry)
```
**Migration steps:**
#### File 1: BountyEntry.cs (Data class — simplest)
```csharp
// RunUO
namespace Server.Custom.BountySystem
{
public class BountyEntry
{
private Mobile m_Target;
private Mobile m_Poster;
private int m_Amount;
private DateTime m_Expiry;
public Mobile Target { get { return m_Target; } }
public Mobile Poster { get { return m_Poster; } }
public int Amount { get { return m_Amount; } set { m_Amount = value; } }
public DateTime Expiry { get { return m_Expiry; } }
public BountyEntry(Mobile target, Mobile poster, int amount, DateTime expiry)
{
m_Target = target;
m_Poster = poster;
m_Amount = amount;
m_Expiry = expiry;
}
}
}
// ModernUO
namespace Server.Custom.BountySystem;
public class BountyEntry
{
public Mobile Target { get; }
public Mobile Poster { get; }
public int Amount { get; set; }
public DateTime Expiry { get; }
public BountyEntry(Mobile target, Mobile poster, int amount, DateTime expiry)
{
Target = target;
Poster = poster;
Amount = amount;
Expiry = expiry;
}
}
```
#### File 2: BountySystem.cs (Persistence — convert to GenericPersistence)
```csharp
// ModernUO
namespace Server.Custom.BountySystem;
public class BountySystem : GenericPersistence
{
private static BountySystem _instance;
private static readonly List<BountyEntry> _bounties = new();
public static IReadOnlyList<BountyEntry> Bounties => _bounties;
public static void Configure()
{
_instance = new BountySystem();
}
public BountySystem() : base("BountySystem", 10) { }
public override void Serialize(IGenericWriter writer)
{
writer.WriteEncodedInt(0);
writer.WriteEncodedInt(_bounties.Count);
foreach (var b in _bounties)
{
writer.Write(b.Target);
writer.Write(b.Poster);
writer.Write(b.Amount);
writer.Write(b.Expiry);
}
}
public override void Deserialize(IGenericReader reader)
{
var version = reader.ReadEncodedInt();
var count = reader.ReadEncodedInt();
for (var i = 0; i < count; i++)
{
var target = reader.ReadEntity<Mobile>();
var poster = reader.ReadEntity<Mobile>();
var amount = reader.ReadInt();
var expiry = reader.ReadDateTime();
if (target != null && poster != null)
_bounties.Add(new BountyEntry(target, poster, amount, expiry));
}
}
public static void AddBounty(BountyEntry entry)
{
_bounties.Add(entry);
_instance.MarkDirty();
}
public static void RemoveBounty(BountyEntry entry)
{
_bounties.Remove(entry);
_instance.MarkDirty();
}
}
```
#### File 3: BountyBoard.cs (Item — serialization + timer)
```csharp
// ModernUO
using ModernUO.Serialization;
using Server.Gumps;
namespace Server.Custom.BountySystem;
[SerializationGenerator(0, false)]
public partial class BountyBoard : Item
{
[Constructible]
public BountyBoard() : base(0x1E5E)
{
Movable = false;
}
public override string DefaultName => "a bounty board";
public override void OnDoubleClick(Mobile from)
{
if (!from.InRange(GetWorldLocation(), 3))
{
from.SendLocalizedMessage(500446);
return;
}
BountyBoardGump.DisplayTo(from);
}
}
```
#### File 4: BountyBoardGump.cs (Gump — convert to DynamicGump)
```csharp
// ModernUO
using Server.Gumps;
namespace Server.Custom.BountySystem;
public class BountyBoardGump : DynamicGump
{
private readonly Mobile _from;
public override bool Singleton => true;
private BountyBoardGump(Mobile from) : base(50, 50)
{
_from = from;
}
protected override void BuildLayout(ref DynamicGumpBuilder builder)
{
var bounties = BountySystem.Bounties;
var height = 80 + bounties.Count * 30;
builder.AddPage();
builder.AddBackground(0, 0, 400, height, 5054);
builder.AddAlphaRegion(10, 10, 380, height - 20);
builder.AddLabel(20, 15, 0x480, "Active Bounties");
for (var i = 0; i < bounties.Count; i++)
{
var b = bounties[i];
var y = 45 + i * 30;
builder.AddLabel(20, y, 0x480, b.Target.Name ?? "Unknown");
builder.AddLabel(200, y, 0x480, $"{b.Amount} gold");
builder.AddButton(350, y, 4005, 4007, i + 1);
}
}
public override void OnResponse(NetState sender, in RelayInfo info)
{
if (info.ButtonID > 0)
{
var index = info.ButtonID - 1;
var bounties = BountySystem.Bounties;
if (index < bounties.Count)
_from.SendMessage($"Bounty on {bounties[index].Target.Name}: {bounties[index].Amount} gold");
}
}
public static void DisplayTo(Mobile from)
{
if (from?.NetState == null)
return;
from.SendGump(new BountyBoardGump(from));
}
}
```
#### File 5: BountyCommands.cs (Commands — minimal changes)
```csharp
// ModernUO
using Server.Commands;
namespace Server.Custom.BountySystem;
public static class BountyCommands
{
public static void Configure()
{
CommandSystem.Register("Bounty", AccessLevel.Player, Bounty_OnCommand);
}
[Usage("Bounty <amount>")]
[Description("Place a bounty on a targeted player")]
public static void Bounty_OnCommand(CommandEventArgs e)
{
if (e.Length < 1)
{
e.Mobile.SendMessage("Usage: [Bounty <amount>");
return;
}
var amount = e.GetInt32(0);
e.Mobile.SendMessage("Target the player to place a bounty on.");
e.Mobile.Target = new BountyTarget(amount);
}
}
```
## Cross-Reference Handling
When files reference each other:
1. Convert shared data types first
2. Convert the persistence/core system next
3. Convert consumers (gumps, commands) last
4. Keep all files in the same namespace
## Common Multi-File Patterns
### Crafting Extension
| RunUO File | ModernUO Conversion |
|---|---|
| CraftItem subclass | `[SerializationGenerator]`, `partial` |
| CraftSystem registration | `Configure()` method |
| Resource definition | Data class, auto-properties |
| Custom tools (Item) | Standard item migration |
### Custom Spawner
| RunUO File | ModernUO Conversion |
|---|---|
| Spawner Item | `[SerializationGenerator]`, `[AfterDeserialization]` for timers |
| Spawn timer | `TimerExecutionToken`, fire-and-forget |
| Config file | `JsonConfig` or `ServerConfiguration` |
| Admin gump | `DynamicGump` conversion |
### Economy/Banking System
| RunUO File | ModernUO Conversion |
|---|---|
| Data persistence | `GenericPersistence` |
| Account data | Serialized fields or `GenericPersistence` |
| Transaction log | `GenericPersistence` |
| Player-facing gump | `DynamicGump` or `StaticGump` |
## Configuration Migration
RunUO systems often use XML or custom config files:
```csharp
// RunUO
XmlDocument doc = new XmlDocument();
doc.Load("Data/MyConfig.xml");
// ModernUO — use ServerConfiguration for simple settings
var enabled = ServerConfiguration.GetOrUpdateSetting("mySystem.enabled", true);
var maxItems = ServerConfiguration.GetOrUpdateSetting("mySystem.maxItems", 100);
// ModernUO — use JsonConfig for complex settings
var config = JsonConfig.Deserialize<MyConfig>(configPath);
```
## Testing the Migration
After converting all files:
1. Compile the project: `dotnet build`
2. Fix any compilation errors
3. Start the server and check for runtime errors
4. Test each feature:
- Items can be `[add`ed
- Gumps display correctly
- Commands work
- Data persists across saves/restarts
- Timers fire correctly
## See Also
- `dev-docs/content-patterns.md` — File organization patterns
- `dev-docs/configuration.md` — Configuration system reference
- All prior migration docs (01 through 09)

View file

@ -0,0 +1,200 @@
# API Reference — RunUO to ModernUO Mapping
## Quick Lookup
Alphabetical by RunUO API name. Use Ctrl+F / Cmd+F to search.
## Attributes
| RunUO | ModernUO | Notes |
|---|---|---|
| `[Constructable]` | `[Constructible]` | Spelling change |
| `[CommandProperty(AccessLevel)]` | `[SerializedCommandProperty(AccessLevel)]` | On `[SerializableField]` fields |
| `[CommandProperty(AccessLevel)]` | `[CommandProperty(AccessLevel)]` | On `[SerializableProperty]` properties (unchanged) |
| `[CorpseName("name")]` | `public override string CorpseName => "name";` | Attribute → property override |
| `[Constructable]` | `[Constructible]` | Must use `using ModernUO.Serialization;` |
| `[Usage("...")]` | `[Usage("...")]` | Unchanged |
| `[Description("...")]` | `[Description("...")]` | Unchanged |
| N/A | `[Aliases("a1", "a2")]` | New in ModernUO |
| N/A | `[SerializationGenerator(ver, enc)]` | New — replaces manual Serialize/Deserialize |
| N/A | `[SerializableField(idx)]` | New — auto-generates property + serialization |
| N/A | `[SerializableProperty(idx)]` | New — custom property serialization |
| N/A | `[InvalidateProperties]` | New — auto-calls InvalidateProperties on set |
| N/A | `[AfterDeserialization]` | New — post-deserialize hook |
| N/A | `[DeltaDateTime]` | New — relative DateTime storage |
| N/A | `[EncodedInt]` | New — variable-length int encoding |
| N/A | `[InternString]` | New — string interning on load |
| N/A | `[Tidy]` | New — auto-clean null entries in collections |
| N/A | `[CanBeNull]` | New — nullable reference field |
| N/A | `[TypeAlias("old.name")]` | New — backward-compatible type mapping |
## Classes
| RunUO | ModernUO | Notes |
|---|---|---|
| `BinaryFileWriter` | `IGenericWriter` (via GenericPersistence) | No manual file writing |
| `BinaryFileReader` | `IGenericReader` (via GenericPersistence) | No manual file reading |
| `GenericWriter` | `IGenericWriter` | Interface now |
| `GenericReader` | `IGenericReader` | Interface now |
| `Gump` | `DynamicGump` or `StaticGump<T>` | See 04-gumps.md |
| `ObjectPropertyList` (parameter) | `IPropertyList` | Interface for GetProperties |
| `Packet` (base class) | Removed — use static methods | See 05-packets.md |
| `PacketWriter` | `SpanWriter` | Ref struct |
| `PacketReader` | `SpanReader` | Ref struct |
| `Timer` (subclass pattern) | `Timer.StartTimer()` + `TimerExecutionToken` | See 03-timers.md |
| `TextRelay` | Removed — use `info.GetTextEntry(id)` | Returns string directly |
| `RelayInfo` | `RelayInfo` (passed with `in`) | `in RelayInfo` in OnResponse |
## Constructors
| RunUO | ModernUO | Notes |
|---|---|---|
| `MyItem(Serial serial) : base(serial)` | DELETE | Auto-generated |
| `BaseCreature(AI, Fight, 10, 1, 0.2, 0.4)` | `BaseCreature(AI, Fight)` | Extra params have defaults |
## Events (EventSink)
| RunUO | ModernUO | Signature |
|---|---|---|
| `EventSink.Login` | `EventSink.Connected` | `LoginEventArgs``Action<Mobile>` |
| `EventSink.Logout` | `EventSink.Disconnected` | `LogoutEventArgs``Action<Mobile>` |
| `EventSink.WorldSave` | `EventSink.WorldSave` | `WorldSaveEventArgs``Action` |
| `EventSink.WorldLoad` | `EventSink.WorldLoad` | `Action` |
| `EventSink.ServerStarted` | `EventSink.ServerStarted` | `Action` |
| `EventSink.Crashed` | `EventSink.ServerCrashed` | Renamed |
| `EventSink.Speech` | `EventSink.Speech` | `Action<SpeechEventArgs>` |
| `EventSink.Movement` | `EventSink.Movement` | `Action<MovementEventArgs>` |
| `EventSink.AggressiveAction` | `EventSink.AggressiveAction` | `Action<AggressiveActionEventArgs>` |
| `EventSink.AccountLogin` | `EventSink.AccountLogin` | `Action<AccountLoginEventArgs>` |
| `EventSink.SocketConnect` | `EventSink.SocketConnect` | `Action<SocketConnectEventArgs>` |
| `EventSink.Shutdown` | `EventSink.Shutdown` | `Action` |
| `EventSink.BeforeWorldSave` | Removed | Use `WorldSave` |
| `EventSink.PlayerDeath` | `PlayerMobile.PlayerDeathEvent` | CodeGeneratedEvent |
| `EventSink.CreatureDeath` | `BaseCreature.CreatureDeathEvent` | CodeGeneratedEvent |
## Event Delegates
| RunUO | ModernUO |
|---|---|
| `new WorldSaveEventHandler(Save)` | `Save` (direct method reference) |
| `new WorldLoadEventHandler(Load)` | `Load` |
| `new LoginEventHandler(OnLogin)` | `OnConnected` |
| `new LogoutEventHandler(OnLogout)` | `OnDisconnected` |
| `new SpeechEventHandler(OnSpeech)` | `OnSpeech` |
| `new TimerCallback(Method)` | `Method` |
| `new TimerStateCallback(Method)` | `Method` (typed overloads) |
| `new OnPacketReceive(Handler)` | `&Handler` (function pointer) |
## Gump Methods
| RunUO | ModernUO | Notes |
|---|---|---|
| `AddPage(0)` | `builder.AddPage()` | 0 is default |
| `AddBackground(...)` | `builder.AddBackground(...)` | On builder |
| `AddAlphaRegion(...)` | `builder.AddAlphaRegion(...)` | On builder |
| `AddLabel(x, y, hue, text)` | `builder.AddLabel(x, y, hue, text)` | On builder |
| `AddHtml(x, y, w, h, text, bg, scroll)` | `builder.AddHtml(x, y, w, h, text, background: bg, scrollbar: scroll)` | Named params |
| `AddHtmlLocalized(...)` | `builder.AddHtmlLocalized(...)` | On builder |
| `AddButton(x, y, n, p, id, Reply, 0)` | `builder.AddButton(x, y, n, p, id)` | Reply is default |
| `AddTextEntry(...)` | `builder.AddTextEntry(...)` | On builder |
| `AddCheck(x, y, off, on, state, id)` | `builder.AddCheckbox(x, y, off, on, state, id)` | Renamed |
| `Closable = false` | `builder.SetNoClose()` | Property → method |
| `Dragable = false` | `builder.SetNoMove()` | Renamed |
| `Resizable = false` | `builder.SetNoResize()` | Property → method |
| `Disposable = false` | `builder.SetNoDispose()` | Property → method |
| `from.SendGump(new G(...))` | `G.DisplayTo(from, ...)` | Static entry point |
| `from.CloseGump(typeof(G))` | `from.CloseGump<G>()` | Generic |
| `from.HasGump(typeof(G))` | `from.HasGump<G>()` | Generic |
## Gump Response
| RunUO | ModernUO |
|---|---|
| `OnResponse(NetState, RelayInfo)` | `OnResponse(NetState, in RelayInfo)` |
| `info.TextEntries[i].Text` | `info.GetTextEntry(id)` |
| `info.IsSwitched(id)` | `info.IsSwitched(id)` (same) |
| `info.ButtonID` | `info.ButtonID` (same) |
## Methods
| RunUO | ModernUO | Notes |
|---|---|---|
| `DateTime.UtcNow` | `Core.Now` | Server time source |
| `Console.WriteLine(...)` | `logger.Information(...)` | Structured logging |
| `World.FindMobile(serial)` | `reader.ReadEntity<Mobile>()` | In deserialization |
| `World.FindItem(serial)` | `reader.ReadEntity<Item>()` | In deserialization |
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` | Generic method |
| `reader.ReadItem()` | `reader.ReadEntity<Item>()` | Generic method |
| `reader.ReadInt()` | `reader.ReadInt()` | Same |
| `writer.Write((int)value)` | `writer.Write(value)` | No cast needed |
| `writer.WriteEncodedInt(value)` | `writer.WriteEncodedInt(value)` | Same |
| `InvalidateProperties()` | `InvalidateProperties()` | Same, or use `[InvalidateProperties]` |
| `this.MarkDirty()` | `this.MarkDirty()` | NEW — required in custom setters |
## Networking
| RunUO | ModernUO | Notes |
|---|---|---|
| `ns.Send(new MyPacket(...))` | `ns.SendMyPacket(...)` | Extension method |
| `Packet.Compile()` | Not needed | Stack-allocated |
| `Packet.SetStatic()` | Not needed | Reuse via buffer[0] != 0 guard |
| `PacketHandlers.Register(...)` | `IncomingPackets.Register(...)` | Function pointers |
| `m_Stream.Write(value)` | `writer.Write(value)` | SpanWriter |
| `pvSrc.ReadInt32()` | `reader.ReadInt32()` | SpanReader |
| `pvSrc.ReadString()` | `reader.ReadAsciiSafe()` | Explicit encoding |
| `pvSrc.ReadUnicodeStringSafe()` | `reader.ReadBigUniSafe()` | Explicit name |
## Persistence
| RunUO | ModernUO | Notes |
|---|---|---|
| `EventSink.WorldSave += Save` | `class X : GenericPersistence` | Subclass |
| `EventSink.WorldLoad += Load` | `override Deserialize(IGenericReader)` | Method |
| `new BinaryFileWriter(path, true)` | Handled by framework | Automatic |
| `BinaryFileReader` / `FileStream` | Handled by framework | Automatic |
| `Directory.CreateDirectory(...)` | Handled by framework | Automatic |
## Properties
| RunUO | ModernUO | Notes |
|---|---|---|
| `Name = "text"` (in constructor) | `public override string DefaultName => "text";` | Property override |
| `Name` (custom text) | `DefaultName` (property override) | For fixed names |
| `int Prop { get { return m_X; } }` | `int Prop => _x;` | Expression-bodied |
| `int Prop { get { return m_X; } set { m_X = value; } }` | Auto-generated by `[SerializableField]` | Delete manual property |
## Timer
| RunUO | ModernUO | Notes |
|---|---|---|
| `new InternalTimer().Start()` | `Timer.StartTimer(..., out token)` | Fire-and-forget |
| `Timer.DelayCall(delay, callback)` | `Timer.StartTimer(delay, callback)` | Similar |
| `Timer.DelayCall(delay, stateCallback, state)` | `Timer.DelayCall(delay, callback, state)` | Typed state |
| `timer.Stop()` | `token.Cancel()` | Struct, safe to call multiple times |
| `timer.Running` | `token.Running` | Same concept |
| `TimerPriority.XXX` | Removed | Timer wheel auto-schedules |
| Timer in `Deserialize()` | `[AfterDeserialization]` | Post-load hook |
| `timer != null` check | `token.Running` check | Value type, always valid |
## Threading / Performance
| RunUO | ModernUO | Notes |
|---|---|---|
| `lock (_obj)` | Remove | Single-threaded |
| `volatile` | Remove keyword | Single-threaded |
| `ConcurrentDictionary` | `Dictionary` | Single-threaded |
| `Task.Run(...)` | Use `Timer.StartTimer()` | Game loop only |
| `new Thread(...)` | Forbidden | Game loop only |
| `ArrayPool<T>.Shared` | `STArrayPool<T>.Shared` | No-lock pool |
| `new List<T>()` (hot path) | `PooledRefList<T>.Create()` | Zero-alloc |
| `World.Mobiles.Values` iteration | `map.GetMobilesInRange<T>()` | Spatial query |
| `World.Items.Values` iteration | `map.GetItemsInRange<T>()` | Spatial query |
## Usings
| RunUO | ModernUO | Notes |
|---|---|---|
| (implicit) | `using ModernUO.Serialization;` | For serialization attributes |
| (implicit) | `using Server.Logging;` | For logging |
| (implicit) | `using Server.Gumps;` | For gump extension methods |
| (implicit) | `using Server.Collections;` | For PooledRefList |