From 4f9bc1d9f6c759b172dcba6fd9f77d1639f996cb Mon Sep 17 00:00:00 2001 From: Kamron Batman <3953314+kamronbatman@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:33:45 -0700 Subject: [PATCH] feat: Adds AI skills to migrate from RunUO (#2366) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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`/`DynamicGump` with builders - `EventSink.WorldSave` → `GenericPersistence` - `ObjectPropertyList` → `IPropertyList` with string hole rules - Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries --- CLAUDE.md | 15 + .../migrate-commands-events.md | 39 ++ .../migrate-from-runuo/migrate-foundation.md | 50 ++ .../migrate-from-runuo/migrate-gumps.md | 45 ++ .../migrate-items-mobiles.md | 43 ++ .../migrate-from-runuo/migrate-packets.md | 44 ++ .../migrate-from-runuo/migrate-persistence.md | 42 ++ .../migrate-property-lists.md | 33 + .../migrate-serialization.md | 51 ++ .../migrate-from-runuo/migrate-systems.md | 45 ++ .../migrate-from-runuo/migrate-timers.md | 44 ++ .../claude-skills/modernuo-serialization.md | 42 ++ dev-docs/claude-skills/modernuo-threading.md | 27 +- dev-docs/claude-skills/modernuo-timers.md | 2 + dev-docs/runuo-migration-docs/00-overview.md | 68 +++ .../01-foundation-changes.md | 308 ++++++++++ .../runuo-migration-docs/02-serialization.md | 423 +++++++++++++ dev-docs/runuo-migration-docs/03-timers.md | 327 ++++++++++ dev-docs/runuo-migration-docs/04-gumps.md | 343 +++++++++++ .../05-packets-networking.md | 309 ++++++++++ .../runuo-migration-docs/06-property-lists.md | 194 ++++++ .../07-commands-events.md | 211 +++++++ .../runuo-migration-docs/08-persistence.md | 398 ++++++++++++ .../09-items-mobiles-creatures.md | 566 ++++++++++++++++++ .../10-systems-engines.md | 354 +++++++++++ .../runuo-migration-docs/11-api-reference.md | 200 +++++++ 26 files changed, 4216 insertions(+), 7 deletions(-) create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-commands-events.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-foundation.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-persistence.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-property-lists.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md create mode 100644 dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md create mode 100644 dev-docs/runuo-migration-docs/00-overview.md create mode 100644 dev-docs/runuo-migration-docs/01-foundation-changes.md create mode 100644 dev-docs/runuo-migration-docs/02-serialization.md create mode 100644 dev-docs/runuo-migration-docs/03-timers.md create mode 100644 dev-docs/runuo-migration-docs/04-gumps.md create mode 100644 dev-docs/runuo-migration-docs/05-packets-networking.md create mode 100644 dev-docs/runuo-migration-docs/06-property-lists.md create mode 100644 dev-docs/runuo-migration-docs/07-commands-events.md create mode 100644 dev-docs/runuo-migration-docs/08-persistence.md create mode 100644 dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md create mode 100644 dev-docs/runuo-migration-docs/10-systems-engines.md create mode 100644 dev-docs/runuo-migration-docs/11-api-reference.md diff --git a/CLAUDE.md b/CLAUDE.md index 79a45d087..bfbb83a14 100644 --- a/CLAUDE.md +++ b/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/.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). diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-commands-events.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-commands-events.md new file mode 100644 index 000000000..21d8cdc69 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-commands-events.md @@ -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) | +| `EventSink.Logout` | `EventSink.Disconnected` (Action) | +| `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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-foundation.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-foundation.md new file mode 100644 index 000000000..630d8eb45 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-foundation.md @@ -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()`) +8. Remove `lock`, `volatile`, `ConcurrentDictionary`, `Mutex` -- server is single-threaded +9. Remove `Task.Run`, `new Thread` -- use `Timer.StartTimer()` instead +10. `ArrayPool.Shared` -> `STArrayPool.Shared` +11. `new List()` on hot paths -> `PooledRefList.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.Shared` | `STArrayPool.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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md new file mode 100644 index 000000000..acb7452ae --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md @@ -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` (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` | +| `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()` | + +## 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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md new file mode 100644 index 000000000..0155f2974 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md @@ -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` -> `ref PooledRefList` + +## 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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md new file mode 100644 index 000000000..c686908ff --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md @@ -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 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)` 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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-persistence.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-persistence.md new file mode 100644 index 000000000..0fbe257a6 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-persistence.md @@ -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()` + +## 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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-property-lists.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-property-lists.md new file mode 100644 index 000000000..02cc7dae7 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-property-lists.md @@ -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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md new file mode 100644 index 000000000..4d74bccb0 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-serialization.md @@ -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()` | +| `reader.ReadItem()` | `reader.ReadEntity()` | + +## 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) diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md new file mode 100644 index 000000000..ad55466d5 --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md @@ -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 diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md new file mode 100644 index 000000000..ed1b715fa --- /dev/null +++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md @@ -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 diff --git a/dev-docs/claude-skills/modernuo-serialization.md b/dev-docs/claude-skills/modernuo-serialization.md index 8259d0961..d09068c06 100644 --- a/dev-docs/claude-skills/modernuo-serialization.md +++ b/dev-docs/claude-skills/modernuo-serialization.md @@ -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 diff --git a/dev-docs/claude-skills/modernuo-threading.md b/dev-docs/claude-skills/modernuo-threading.md index 273fc038b..800bf77d2 100644 --- a/dev-docs/claude-skills/modernuo-threading.md +++ b/dev-docs/claude-skills/modernuo-threading.md @@ -116,19 +116,32 @@ using var list = PooledRefList.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): diff --git a/dev-docs/claude-skills/modernuo-timers.md b/dev-docs/claude-skills/modernuo-timers.md index 7deaaa72d..c9160c8d0 100644 --- a/dev-docs/claude-skills/modernuo-timers.md +++ b/dev-docs/claude-skills/modernuo-timers.md @@ -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` diff --git a/dev-docs/runuo-migration-docs/00-overview.md b/dev-docs/runuo-migration-docs/00-overview.md new file mode 100644 index 000000000..0ef5216be --- /dev/null +++ b/dev-docs/runuo-migration-docs/00-overview.md @@ -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)` methods + `SpanWriter` | +| Gumps | `Gump` with entry lists in constructor | `StaticGump`/`DynamicGump` with builder pattern | +| Property lists | `ObjectPropertyList` directly | `IPropertyList` interface | +| Persistence | `EventSink.WorldSave`/`WorldLoad` with manual binary files | `GenericPersistence`/`GenericEntityPersistence` | +| Events | `EventSink` with delegate types (`WorldSaveEventHandler`) | `EventSink` with `Action` 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()` everywhere | `PooledRefList`, `STArrayPool` | + +## 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 diff --git a/dev-docs/runuo-migration-docs/01-foundation-changes.md b/dev-docs/runuo-migration-docs/01-foundation-changes.md new file mode 100644 index 000000000..0af53c6d7 --- /dev/null +++ b/dev-docs/runuo-migration-docs/01-foundation-changes.md @@ -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(location, 10)) +{ + DoSomething(m); +} +``` + +Available spatial queries (on `Map`): +- `GetMobilesAt(Point3D)` — exact location +- `GetMobilesInRange(Point3D, int range)` — within range +- `GetMobilesInBounds(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 _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.Shared.Rent(1024); +// ... +ArrayPool.Shared.Return(buffer); + +// ModernUO (single-threaded, no locks) +var buffer = STArrayPool.Shared.Rent(1024); +// ... +STArrayPool.Shared.Return(buffer); +``` + +## 10. new List() on Hot Paths → PooledRefList + +```csharp +// RunUO +var list = new List(); +foreach (var m in nearbyMobiles) +{ + if (m.Alive) + list.Add(m); +} +// list goes to GC + +// ModernUO (zero-alloc) +using var list = PooledRefList.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`, `.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.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()` on hot paths with `PooledRefList` +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 diff --git a/dev-docs/runuo-migration-docs/02-serialization.md b/dev-docs/runuo-migration-docs/02-serialization.md new file mode 100644 index 000000000..be6010bbf --- /dev/null +++ b/dev-docs/runuo-migration-docs/02-serialization.md @@ -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()` | Generic method | +| `reader.ReadItem()` | `reader.ReadEntity()` | 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 _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) diff --git a/dev-docs/runuo-migration-docs/03-timers.md b/dev-docs/runuo-migration-docs/03-timers.md new file mode 100644 index 000000000..8331d470b --- /dev/null +++ b/dev-docs/runuo-migration-docs/03-timers.md @@ -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 diff --git a/dev-docs/runuo-migration-docs/04-gumps.md b/dev-docs/runuo-migration-docs/04-gumps.md new file mode 100644 index 000000000..ab44914a8 --- /dev/null +++ b/dev-docs/runuo-migration-docs/04-gumps.md @@ -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` (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 +{ + 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` | 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()` | Generic method | +| `from.HasGump(typeof(MyGump))` | `from.HasGump()` | Generic method | + +## Step-by-Step Conversion + +### Step 1: Choose Target Type +| If the layout... | Convert to | +|---|---| +| Is the same structure every time | `StaticGump` (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 +``` + +### 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(); +``` + +## 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()`, `FindGump()`, `CloseGump()` 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 diff --git a/dev-docs/runuo-migration-docs/05-packets-networking.md b/dev-docs/runuo-migration-docs/05-packets-networking.md new file mode 100644 index 000000000..a722825bd --- /dev/null +++ b/dev-docs/runuo-migration-docs/05-packets-networking.md @@ -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 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)` 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 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 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 diff --git a/dev-docs/runuo-migration-docs/06-property-lists.md b/dev-docs/runuo-migration-docs/06-property-lists.md new file mode 100644 index 000000000..2df7f408f --- /dev/null +++ b/dev-docs/runuo-migration-docs/06-property-lists.md @@ -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 diff --git a/dev-docs/runuo-migration-docs/07-commands-events.md b/dev-docs/runuo-migration-docs/07-commands-events.md new file mode 100644 index 000000000..a3a261bfb --- /dev/null +++ b/dev-docs/runuo-migration-docs/07-commands-events.md @@ -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` 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 ")] +[Description("Does something")] + +// ModernUO — same attributes, plus optional Aliases +[Usage("MyCommand ")] +[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 delegates, changed event names +public static void Configure() +{ + EventSink.WorldSave += OnWorldSave; // Action (no args) + EventSink.WorldLoad += OnWorldLoad; // Action (no args) + EventSink.Connected += OnConnected; // Action — was Login + EventSink.Disconnected += OnDisconnected; // Action — was Logout + EventSink.Speech += OnSpeech; // Action + EventSink.Movement += OnMovement; // Action + EventSink.ServerStarted += OnServerStarted; // Action (no args) + EventSink.ServerCrashed += OnCrashed; // Action +} + +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` | +| `EventSink.Logout` | `EventSink.Disconnected` | `LogoutEventArgs` → `Action` | +| `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 diff --git a/dev-docs/runuo-migration-docs/08-persistence.md b/dev-docs/runuo-migration-docs/08-persistence.md new file mode 100644 index 000000000..dd30668f2 --- /dev/null +++ b/dev-docs/runuo-migration-docs/08-persistence.md @@ -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` (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 m_Records = new Dictionary(); + + 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 _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(); + 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` | +| 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()` | Generic method | +| `World.FindItem(reader.ReadInt32())` | `reader.ReadEntity()` | 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(); // 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()` | +| `World.FindItem(reader.ReadInt32())` | `reader.ReadEntity()` | +| `new DateTime(reader.ReadInt64())` | `reader.ReadDateTime()` | + +## Before/After: Complete System + +**RunUO:** +```csharp +namespace Server.Custom +{ + public class VirtueSystem + { + private static Dictionary m_Points = new Dictionary(); + + 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 _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(); + 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() Returns Null for Deleted Entities +Unlike `World.FindMobile()`, `ReadEntity()` 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 diff --git a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md new file mode 100644 index 000000000..f31556194 --- /dev/null +++ b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md @@ -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 list) +{ + base.GetContextMenuEntries(from, list); + list.Add(new MyEntry(this)); +} + +// ModernUO +public override void GetContextMenuEntries(Mobile from, ref PooledRefList 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 diff --git a/dev-docs/runuo-migration-docs/10-systems-engines.md b/dev-docs/runuo-migration-docs/10-systems-engines.md new file mode 100644 index 000000000..329ff1374 --- /dev/null +++ b/dev-docs/runuo-migration-docs/10-systems-engines.md @@ -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 _bounties = new(); + + public static IReadOnlyList 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(); + var poster = reader.ReadEntity(); + 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 ")] + [Description("Place a bounty on a targeted player")] + public static void Bounty_OnCommand(CommandEventArgs e) + { + if (e.Length < 1) + { + e.Mobile.SendMessage("Usage: [Bounty "); + 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(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) diff --git a/dev-docs/runuo-migration-docs/11-api-reference.md b/dev-docs/runuo-migration-docs/11-api-reference.md new file mode 100644 index 000000000..215dd3b06 --- /dev/null +++ b/dev-docs/runuo-migration-docs/11-api-reference.md @@ -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` | 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` | +| `EventSink.Logout` | `EventSink.Disconnected` | `LogoutEventArgs` → `Action` | +| `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` | +| `EventSink.Movement` | `EventSink.Movement` | `Action` | +| `EventSink.AggressiveAction` | `EventSink.AggressiveAction` | `Action` | +| `EventSink.AccountLogin` | `EventSink.AccountLogin` | `Action` | +| `EventSink.SocketConnect` | `EventSink.SocketConnect` | `Action` | +| `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()` | Generic | +| `from.HasGump(typeof(G))` | `from.HasGump()` | 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()` | In deserialization | +| `World.FindItem(serial)` | `reader.ReadEntity()` | In deserialization | +| `reader.ReadMobile()` | `reader.ReadEntity()` | Generic method | +| `reader.ReadItem()` | `reader.ReadEntity()` | 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.Shared` | `STArrayPool.Shared` | No-lock pool | +| `new List()` (hot path) | `PooledRefList.Create()` | Zero-alloc | +| `World.Mobiles.Values` iteration | `map.GetMobilesInRange()` | Spatial query | +| `World.Items.Values` iteration | `map.GetItemsInRange()` | 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 |