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:
parent
31b22d4773
commit
4f9bc1d9f6
26 changed files with 4216 additions and 7 deletions
15
CLAUDE.md
15
CLAUDE.md
|
|
@ -43,6 +43,8 @@ Apply these when writing or reviewing `.cs` files under `Projects/`.
|
|||
| Configuration system | `dev-docs/configuration.md` |
|
||||
| Networking & packets | `dev-docs/networking-packets.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)
|
||||
|
||||
|
|
@ -71,7 +73,20 @@ Then copy only the relevant skill files based on the task:
|
|||
| Era-conditional code | `modernuo-era-expansion` |
|
||||
| Code review / audit | `modernuo-code-audit` |
|
||||
| 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/`
|
||||
|
||||
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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
45
dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md
Normal file
45
dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md
Normal 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
|
||||
|
|
@ -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
|
||||
44
dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md
Normal file
44
dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
45
dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md
Normal file
45
dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md
Normal 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
|
||||
44
dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md
Normal file
44
dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md
Normal 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
|
||||
|
|
@ -221,10 +221,52 @@ public partial class BagOfSending : Item
|
|||
}
|
||||
```
|
||||
|
||||
## Custom Serialize/Deserialize (Purity Rules)
|
||||
|
||||
When writing custom `Serialize(IGenericWriter)` or `Deserialize(IGenericReader)` methods (e.g. for `GenericPersistence` subclasses), the following rules apply:
|
||||
|
||||
### Serialize() MUST remain pure
|
||||
`Serialize()` is called from **background serialization threads** during world saves (see `SerializationThreadWorker`). Multiple entities are serialized in parallel across threads. This means `Serialize()` must NOT:
|
||||
|
||||
- **Create or destroy Items/Mobiles** -- mutates shared world state
|
||||
- **Move, equip, or unequip Items/Mobiles** -- mutates shared world state
|
||||
- **Start or stop timers** (`Timer.StartTimer`, `Timer.DelayCall`, `_token.Cancel()`) -- timers are NOT thread-safe
|
||||
- **Send packets or modify NetState** -- networking is game-thread-only
|
||||
- **Access or modify other entities' mutable state** -- data race
|
||||
- **Call `Delete()`** on anything -- triggers deletion cascades on wrong thread
|
||||
|
||||
`Serialize()` should ONLY read fields and write them to the `IGenericWriter`. Treat it as a read-only snapshot.
|
||||
|
||||
```csharp
|
||||
// CORRECT -- pure reads and writes only
|
||||
public override void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.WriteEncodedInt(0); // version
|
||||
writer.WriteEncodedInt(_records.Count);
|
||||
foreach (var (key, value) in _records)
|
||||
{
|
||||
writer.Write(key);
|
||||
writer.Write(value);
|
||||
}
|
||||
}
|
||||
|
||||
// WRONG -- side effects in Serialize
|
||||
public override void Serialize(IGenericWriter writer)
|
||||
{
|
||||
CleanupExpiredEntries(); // BAD: mutates state
|
||||
Timer.StartTimer(Recheck); // BAD: not thread-safe
|
||||
writer.Write(_data);
|
||||
}
|
||||
```
|
||||
|
||||
### Deserialize() runs on the game thread
|
||||
`Deserialize()` runs during world load on the main thread, so it CAN create entities and start timers. However, prefer `[AfterDeserialization]` for timer setup to keep deserialization clean.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
|
||||
- **Serializing timers**: `TimerExecutionToken` cannot be serialized
|
||||
- **Side effects in `Serialize()`**: Serialize runs on background threads -- must be pure (no creating/destroying entities, no timer start/stop, no packets)
|
||||
- **Missing `MarkDirty()`**: Custom property setters must call `this.MarkDirty()`
|
||||
- **Wrong field prefix**: Use `_camelCase`, not `m_camelCase` for new fields
|
||||
- **Forgetting `[Constructible]`**: Items/Mobiles need this for `[add` command
|
||||
|
|
|
|||
|
|
@ -116,19 +116,32 @@ using var list = PooledRefList<Mobile>.CreateMT();
|
|||
|
||||
## World Saves
|
||||
|
||||
World saves DO involve background work, but this is handled by server infrastructure:
|
||||
1. **Serialization** happens on the main thread (safe access to game state)
|
||||
2. **Disk I/O** may happen on background threads (no game state access)
|
||||
3. Main thread blocks briefly during serialization snapshot
|
||||
World saves use **parallel serialization threads**. This is critical to understand:
|
||||
|
||||
1. **Preserialize**: Allocates heaps and wakes serialization thread workers (background)
|
||||
2. **Snapshot**: Main thread calls `Persistence.SerializeAll()` which pushes entities into `SerializationThreadWorker` queues (round-robin). Workers call `Serialize(writer)` on **their own background threads** in parallel.
|
||||
3. **Write snapshot**: Disk I/O on background threads after serialization completes
|
||||
|
||||
```csharp
|
||||
// From World.cs -- save flow:
|
||||
World.Save();
|
||||
→ Preserialize() on thread pool (allocate heaps)
|
||||
→ Snapshot() on main thread (serialize game state)
|
||||
→ WriteFiles() on thread pool (disk I/O only)
|
||||
→ Preserialize() on thread pool (allocate heaps, wake serialization workers)
|
||||
→ Snapshot() on main thread (queues entities to workers, workers serialize in parallel)
|
||||
→ SerializationThreadWorker.Execute() calls e.Serialize(writer) on background thread
|
||||
→ PauseSerializationThreads() (wait for workers to finish)
|
||||
→ WriteSnapshot() on thread pool (disk I/O only)
|
||||
```
|
||||
|
||||
### Serialize() runs on background threads
|
||||
Because `SerializationThreadWorker` calls `Serialize()` on its own thread, **`Serialize()` must be pure**:
|
||||
- **NO** creating/destroying Items or Mobiles
|
||||
- **NO** starting/stopping timers (not thread-safe)
|
||||
- **NO** sending packets or modifying NetState
|
||||
- **NO** mutating shared game state
|
||||
- **ONLY** read fields and write to `IGenericWriter`
|
||||
|
||||
See `modernuo-serialization.md` for full purity rules.
|
||||
|
||||
## Exceptions: Server Infrastructure
|
||||
|
||||
These files MAY use threading (they're server infrastructure, not game logic):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ description: >
|
|||
3. **Never serialize `TimerExecutionToken`** -- restore in `[AfterDeserialization]`
|
||||
4. **Always cancel timers in `OnDelete()`/`OnAfterDelete()`**
|
||||
5. **8ms minimum precision** -- timer wheel uses 8ms tick rate
|
||||
6. **Timers are NOT thread-safe** -- never Start/Stop timers or call `Timer.DelayCall`/`Timer.StartTimer` from any thread other than the game thread. This includes `Serialize()` which runs on background serialization threads during world saves.
|
||||
|
||||
## Preferred APIs (In Order)
|
||||
|
||||
|
|
@ -193,6 +194,7 @@ _decayTimer = null;
|
|||
- **Forgetting cleanup**: Timers keep running if not cancelled on deletion
|
||||
- **Using `Thread.Sleep`**: Blocks the game loop. Use `Timer.StartTimer` or `await Timer.Pause` instead
|
||||
- **Creating timers in constructors called during deserialization**: Use `[AfterDeserialization]` instead
|
||||
- **Starting/stopping timers in `Serialize()`**: `Serialize()` runs on background threads during world saves -- timer APIs are game-thread-only and will corrupt state or crash
|
||||
|
||||
## Real Examples
|
||||
- Token cleanup: `Projects/UOContent/Spells/Third/WallOfStone.cs`
|
||||
|
|
|
|||
68
dev-docs/runuo-migration-docs/00-overview.md
Normal file
68
dev-docs/runuo-migration-docs/00-overview.md
Normal 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
|
||||
308
dev-docs/runuo-migration-docs/01-foundation-changes.md
Normal file
308
dev-docs/runuo-migration-docs/01-foundation-changes.md
Normal 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
|
||||
423
dev-docs/runuo-migration-docs/02-serialization.md
Normal file
423
dev-docs/runuo-migration-docs/02-serialization.md
Normal 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)
|
||||
327
dev-docs/runuo-migration-docs/03-timers.md
Normal file
327
dev-docs/runuo-migration-docs/03-timers.md
Normal 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
|
||||
343
dev-docs/runuo-migration-docs/04-gumps.md
Normal file
343
dev-docs/runuo-migration-docs/04-gumps.md
Normal 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
|
||||
309
dev-docs/runuo-migration-docs/05-packets-networking.md
Normal file
309
dev-docs/runuo-migration-docs/05-packets-networking.md
Normal 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
|
||||
194
dev-docs/runuo-migration-docs/06-property-lists.md
Normal file
194
dev-docs/runuo-migration-docs/06-property-lists.md
Normal 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
|
||||
211
dev-docs/runuo-migration-docs/07-commands-events.md
Normal file
211
dev-docs/runuo-migration-docs/07-commands-events.md
Normal 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
|
||||
398
dev-docs/runuo-migration-docs/08-persistence.md
Normal file
398
dev-docs/runuo-migration-docs/08-persistence.md
Normal 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
|
||||
566
dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md
Normal file
566
dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md
Normal 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
|
||||
354
dev-docs/runuo-migration-docs/10-systems-engines.md
Normal file
354
dev-docs/runuo-migration-docs/10-systems-engines.md
Normal 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)
|
||||
200
dev-docs/runuo-migration-docs/11-api-reference.md
Normal file
200
dev-docs/runuo-migration-docs/11-api-reference.md
Normal 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 |
|
||||
Loading…
Add table
Add a link
Reference in a new issue