feat: Adds AI skills to migrate from RunUO (#2366)
## Summary Adds comprehensive RunUO → ModernUO migration documentation and Claude AI skills to help shard owners and script authors convert RunUO 2.7 code to ModernUO. - **10 migration skills** (`dev-docs/claude-skills/migrate-from-runuo/`) — system-by-system conversion guides (foundation, serialization, timers, gumps, packets, property lists, commands/events, persistence, items/mobiles, systems/engines) - **12 reference docs** (`dev-docs/runuo-migration-docs/`) — deep-reference with before/after examples, API mapping tables, edge cases, and gotchas - **Updated existing skills** — `modernuo-timers`, `modernuo-serialization`, and `modernuo-threading` now document that `Serialize()` runs on background threads and timers are not thread-safe - **Updated `CLAUDE.md`** — added migration skill lookup table ### Key migration patterns covered - Manual `Serialize()`/`Deserialize()` → source-generated `[SerializableField]` - `Packet` class hierarchy → static `SpanWriter`/`SpanReader` methods - `Timer` subclasses → `TimerExecutionToken` fire-and-forget - `Gump` → `StaticGump<T>`/`DynamicGump` with builders - `EventSink.WorldSave` → `GenericPersistence` - `ObjectPropertyList` → `IPropertyList` with string hole rules - Universal changes: naming (`m_` → `_`), `[Constructable]` → `[Constructible]`, logging, spatial queries
This commit is contained in:
parent
31b22d4773
commit
4f9bc1d9f6
26 changed files with 4216 additions and 7 deletions
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
name: migrate-commands-events
|
||||
description: >
|
||||
Trigger: when converting RunUO command registration, EventSink handlers, or event delegate patterns.
|
||||
Covers: event name changes, delegate removal, Configure vs Initialize.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Commands & Events Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `EventSink` subscriptions
|
||||
- Converting event handler signatures
|
||||
- Moving from `Initialize()` to `Configure()`
|
||||
- Converting WorldSave/WorldLoad events to GenericPersistence
|
||||
|
||||
## Conversion Steps
|
||||
1. Change `Initialize()` to `Configure()` for event registration
|
||||
2. Remove delegate constructors: `new LoginEventHandler(OnLogin)` -> `OnLogin`
|
||||
3. Rename events: `Login` -> `Connected`, `Logout` -> `Disconnected`
|
||||
4. Update signatures: `OnLogin(LoginEventArgs e)` -> `OnConnected(Mobile m)`
|
||||
5. WorldSave persistence -> convert to `GenericPersistence` (see migrate-persistence skill)
|
||||
|
||||
## Event Mapping
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `EventSink.Login` | `EventSink.Connected` (Action<Mobile>) |
|
||||
| `EventSink.Logout` | `EventSink.Disconnected` (Action<Mobile>) |
|
||||
| `EventSink.WorldSave` | `EventSink.WorldSave` (Action -- no args) |
|
||||
| `EventSink.Crashed` | `EventSink.ServerCrashed` |
|
||||
| `new XXXEventHandler(method)` | `method` (direct reference) |
|
||||
|
||||
## Commands (Minimal Changes)
|
||||
Commands use the same `CommandSystem.Register()` API. Main change: use `Configure()` for registration.
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/07-commands-events.md` -- detailed migration reference
|
||||
- `dev-docs/events.md` -- complete ModernUO event system
|
||||
- `dev-docs/claude-skills/modernuo-events.md` -- ModernUO events skill
|
||||
- `dev-docs/claude-skills/modernuo-commands-targeting.md` -- ModernUO commands skill
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
name: migrate-foundation
|
||||
description: >
|
||||
Trigger: when migrating ANY RunUO code to ModernUO. Always load this skill first.
|
||||
Covers: namespace changes, naming conventions, attribute renames, logging, threading, performance.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Foundation Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting ANY RunUO 2.7 script to ModernUO
|
||||
- Always apply these changes FIRST before system-specific migration
|
||||
|
||||
## Universal Changes Checklist
|
||||
1. File-scoped namespace: `namespace X { ... }` -> `namespace X;`
|
||||
2. `using ModernUO.Serialization;` -- add for any serializable type
|
||||
3. Rename fields: `m_FieldName` -> `_fieldName`
|
||||
4. `[Constructable]` -> `[Constructible]`
|
||||
5. `Console.WriteLine` -> `LogFactory.GetLogger(typeof(X))` -> `logger.Information(...)`
|
||||
6. `DateTime.UtcNow` -> `Core.Now`
|
||||
7. `World.Mobiles`/`World.Items` iteration -> spatial queries (`map.GetMobilesInRange<T>()`)
|
||||
8. Remove `lock`, `volatile`, `ConcurrentDictionary`, `Mutex` -- server is single-threaded
|
||||
9. Remove `Task.Run`, `new Thread` -- use `Timer.StartTimer()` instead
|
||||
10. `ArrayPool<T>.Shared` -> `STArrayPool<T>.Shared`
|
||||
11. `new List<T>()` on hot paths -> `PooledRefList<T>.Create()`
|
||||
12. Modernize property syntax: `{ get { return x; } }` -> `{ get => x; }`
|
||||
13. Delete `MyType(Serial serial) : base(serial)` constructor -- auto-generated
|
||||
14. `Name = "text"` in constructor -> `public override string DefaultName => "text";`
|
||||
|
||||
## Quick Reference
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `[Constructable]` | `[Constructible]` |
|
||||
| `m_Field` | `_field` |
|
||||
| `Console.WriteLine(...)` | `logger.Information(...)` |
|
||||
| `DateTime.UtcNow` | `Core.Now` |
|
||||
| `MyItem(Serial serial) : base(serial)` | DELETE |
|
||||
| `lock (_obj) { }` | Remove entirely |
|
||||
| `ConcurrentDictionary` | `Dictionary` |
|
||||
| `ArrayPool<T>.Shared` | `STArrayPool<T>.Shared` |
|
||||
|
||||
## Anti-Patterns
|
||||
- Don't rename existing `m_` fields in code you're not otherwise migrating
|
||||
- Don't add threading constructs -- everything is single-threaded
|
||||
- Don't use allocating LINQ (`.ToList()`, `.GroupBy()`, etc.) on hot paths
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/01-foundation-changes.md` -- complete foundation changes reference
|
||||
- `dev-docs/code-standards.md` -- ModernUO coding standards and LINQ tiers
|
||||
- `dev-docs/threading-model.md` -- Why single-threaded, what's allowed
|
||||
45
dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md
Normal file
45
dev-docs/claude-skills/migrate-from-runuo/migrate-gumps.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
name: migrate-gumps
|
||||
description: >
|
||||
Trigger: when converting RunUO Gump classes, OnResponse handlers, or gump UI code to ModernUO DynamicGump/StaticGump.
|
||||
Covers: builder pattern, DisplayTo, response handling, empty gump rule.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Gump Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `Gump` subclasses
|
||||
- Converting `OnResponse(NetState, RelayInfo)` handlers
|
||||
- Updating gump sending/closing patterns
|
||||
|
||||
## Conversion Steps
|
||||
1. Choose type: `DynamicGump` (variable layout) or `StaticGump<T>` (fixed layout)
|
||||
2. Change class declaration: `class X : Gump` -> `class X : DynamicGump`
|
||||
3. Add `public override bool Singleton => true;` if only one per player
|
||||
4. Make constructor private, add static `DisplayTo()` method
|
||||
5. Move all `AddXxx()` calls from constructor to `BuildLayout(ref DynamicGumpBuilder builder)`
|
||||
6. Prefix each call with `builder.`: `AddLabel(...)` -> `builder.AddLabel(...)`
|
||||
7. Convert properties: `Closable = false` -> `builder.SetNoClose()`
|
||||
8. Update OnResponse: `OnResponse(NetState, RelayInfo)` -> `OnResponse(NetState, in RelayInfo)`
|
||||
9. Update text entries: `info.TextEntries[i].Text` -> `info.GetTextEntry(id)`
|
||||
10. For StaticGump: extract variable text into placeholders + `BuildStrings`
|
||||
|
||||
## Quick Mapping
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `class X : Gump` | `class X : DynamicGump` or `StaticGump<X>` |
|
||||
| `AddPage(0)` | `builder.AddPage()` |
|
||||
| `Closable = false` | `builder.SetNoClose()` |
|
||||
| `Dragable = false` | `builder.SetNoMove()` |
|
||||
| `OnResponse(NetState, RelayInfo)` | `OnResponse(NetState, in RelayInfo)` |
|
||||
| `info.TextEntries[i].Text` | `info.GetTextEntry(id)` |
|
||||
| `from.SendGump(new X(...))` | `X.DisplayTo(from, ...)` |
|
||||
| `from.CloseGump(typeof(X))` | `from.CloseGump<X>()` |
|
||||
|
||||
## Critical: Empty Gump Rule
|
||||
Never create a gump with no visual elements. Use the `DisplayTo()` pattern -- validate before constructing.
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/04-gumps.md` -- detailed migration reference
|
||||
- `dev-docs/gump-system.md` -- complete ModernUO gump system
|
||||
- `dev-docs/claude-skills/modernuo-gump-system.md` -- ModernUO gump skill
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
name: migrate-items-mobiles
|
||||
description: >
|
||||
Trigger: when converting RunUO Item, Mobile, or BaseCreature subclasses to ModernUO. Most common migration task.
|
||||
Covers: complete item/creature conversion combining serialization, timers, properties, naming.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Item/Mobile/Creature Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting any `Item` subclass from RunUO
|
||||
- Converting any `Mobile`/`BaseCreature` subclass
|
||||
- This is the most common migration task -- combines all other systems
|
||||
|
||||
## Item Conversion Checklist
|
||||
1. [ ] Foundation: file-scoped namespace, `using ModernUO.Serialization;`
|
||||
2. [ ] Class: add `[SerializationGenerator(0, false)]`, add `partial`
|
||||
3. [ ] Fields: `m_X` -> `[SerializableField(N)] _x` with `[SerializedCommandProperty]`
|
||||
4. [ ] Add `[InvalidateProperties]` where RunUO setter called `InvalidateProperties()`
|
||||
5. [ ] Delete: Serial constructor, Serialize, Deserialize
|
||||
6. [ ] `[Constructable]` -> `[Constructible]`
|
||||
7. [ ] `Name = "text"` -> `public override string DefaultName => "text";`
|
||||
8. [ ] Timers: nested class -> `Timer.StartTimer()` + `TimerExecutionToken` + `[AfterDeserialization]` + `OnAfterDelete`
|
||||
9. [ ] Properties: `GetProperties(ObjectPropertyList)` -> `GetProperties(IPropertyList)`, apply string hole rule
|
||||
10. [ ] Context menus: `List<ContextMenuEntry>` -> `ref PooledRefList<ContextMenuEntry>`
|
||||
|
||||
## Creature-Specific Changes
|
||||
- `[CorpseName("...")]` attribute -> `public override string CorpseName => "...";`
|
||||
- `BaseCreature(AI, Fight, 10, 1, 0.2, 0.4)` -> `BaseCreature(AI, Fight)` (extra params default)
|
||||
- `Name = "text"` -> `public override string DefaultName => "text";`
|
||||
- Expression-bodied overrides: `public override int Meat { get { return 1; } }` -> `public override int Meat => 1;`
|
||||
|
||||
## Anti-Patterns
|
||||
- Using `_field--` instead of `Property--` (bypasses MarkDirty tracking)
|
||||
- Forgetting `[AfterDeserialization]` for timer restoration
|
||||
- Forgetting `OnAfterDelete()` for timer cancellation
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` -- detailed migration with before/after examples
|
||||
- `dev-docs/content-patterns.md` -- ModernUO content templates
|
||||
- `dev-docs/claude-skills/modernuo-content-patterns.md` -- ModernUO content skill
|
||||
- `dev-docs/claude-skills/modernuo-serialization.md` -- serialization patterns
|
||||
- `dev-docs/claude-skills/modernuo-timers.md` -- timer patterns
|
||||
44
dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md
Normal file
44
dev-docs/claude-skills/migrate-from-runuo/migrate-packets.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: migrate-packets
|
||||
description: >
|
||||
Trigger: when converting RunUO Packet subclasses, PacketWriter/PacketReader, or packet handler registration to ModernUO span-based packets.
|
||||
Covers: outgoing packet conversion, incoming handler conversion, SpanWriter/SpanReader.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Packet Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `Packet` subclasses to static create methods
|
||||
- Converting `PacketHandlers.Register()` to `IncomingPackets.Register()`
|
||||
- Converting `PacketWriter`/`PacketReader` to `SpanWriter`/`SpanReader`
|
||||
|
||||
## Conversion Steps (Outgoing)
|
||||
1. Create static class: `public static class OutgoingMyPackets`
|
||||
2. Define length constant
|
||||
3. Convert constructor to `static void CreateXxx(Span<byte> buffer, ...)`
|
||||
4. Replace `m_Stream.Write(x)` with `writer.Write(x)`
|
||||
5. Create `SendXxx(this NetState ns, ...)` extension method
|
||||
6. Check `ns.CannotSendPackets()` at start
|
||||
7. Use `stackalloc byte[length].InitializePacket()` for buffer
|
||||
8. Replace `ns.Send(new Packet(...))` with `ns.SendXxx(...)`
|
||||
|
||||
## Conversion Steps (Incoming)
|
||||
1. Change `PacketHandlers.Register(id, len, ingame, new OnPacketReceive(H))` to `IncomingPackets.Register(id, len, ingame, &H)` in `unsafe Configure()`
|
||||
2. Change handler: `void H(NetState, PacketReader)` -> `void H(NetState, SpanReader)`
|
||||
3. Replace read calls: `pvSrc.ReadString()` -> `reader.ReadAsciiSafe()`
|
||||
|
||||
## Quick Mapping
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `class X : Packet` | Static `CreateX(Span<byte>)` method |
|
||||
| `m_Stream.Write(val)` | `writer.Write(val)` (SpanWriter) |
|
||||
| `PacketWriter` | `SpanWriter` |
|
||||
| `PacketReader` / `pvSrc` | `SpanReader` / `reader` |
|
||||
| `ns.Send(new X(...))` | `ns.SendX(...)` extension |
|
||||
| `PacketHandlers.Register(...)` | `IncomingPackets.Register(... &handler)` |
|
||||
| `new OnPacketReceive(H)` | `&H` (function pointer) |
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/05-packets-networking.md` -- detailed migration reference
|
||||
- `dev-docs/networking-packets.md` -- complete ModernUO networking system
|
||||
- `dev-docs/claude-skills/modernuo-networking.md` -- ModernUO networking skill
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
---
|
||||
name: migrate-persistence
|
||||
description: >
|
||||
Trigger: when converting RunUO EventSink.WorldSave/WorldLoad manual binary persistence to ModernUO GenericPersistence.
|
||||
Covers: GenericPersistence subclassing, IGenericWriter/IGenericReader, MarkDirty pattern.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Persistence Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `EventSink.WorldSave`/`EventSink.WorldLoad` patterns
|
||||
- Converting manual `BinaryFileWriter`/`BinaryFileReader` persistence
|
||||
- Systems that save custom data outside of Item/Mobile serialization
|
||||
|
||||
## Conversion Steps
|
||||
1. Create class inheriting `GenericPersistence`: `class MySystem : GenericPersistence`
|
||||
2. Add static instance + `Configure()`: `_instance = new MySystem();`
|
||||
3. Call `base("SaveName", 10)` in constructor
|
||||
4. Move save logic to `override Serialize(IGenericWriter writer)`
|
||||
5. Move load logic to `override Deserialize(IGenericReader reader)`
|
||||
6. Remove `EventSink.WorldSave`/`WorldLoad` subscriptions
|
||||
7. Remove all file management code (Directory.Create, File.Exists, FileStream)
|
||||
8. Add `_instance.MarkDirty()` wherever data changes
|
||||
9. Replace `writer.Write(mobile.Serial.Value)` -> `writer.Write(mobile)`
|
||||
10. Replace `World.FindMobile(reader.ReadInt32())` -> `reader.ReadEntity<Mobile>()`
|
||||
|
||||
## Template
|
||||
```csharp
|
||||
public class MySystem : GenericPersistence
|
||||
{
|
||||
private static MySystem _instance;
|
||||
public static void Configure() => _instance = new MySystem();
|
||||
public MySystem() : base("MySystem", 10) { }
|
||||
public override void Serialize(IGenericWriter writer) { /* save */ }
|
||||
public override void Deserialize(IGenericReader reader) { /* load */ }
|
||||
}
|
||||
```
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/08-persistence.md` -- detailed migration reference with before/after
|
||||
- `dev-docs/serialization.md` -- ModernUO serialization system (IGenericWriter/IGenericReader)
|
||||
- `dev-docs/claude-skills/modernuo-serialization.md` -- ModernUO serialization skill
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
---
|
||||
name: migrate-property-lists
|
||||
description: >
|
||||
Trigger: when converting RunUO GetProperties(ObjectPropertyList) to ModernUO GetProperties(IPropertyList). Critical string literal rule.
|
||||
Covers: IPropertyList interface, string hole rule, cliloc arguments.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Property List Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `GetProperties(ObjectPropertyList list)` overrides
|
||||
- Updating tooltip/property list code
|
||||
|
||||
## Conversion Steps
|
||||
1. Change signature: `GetProperties(ObjectPropertyList list)` -> `GetProperties(IPropertyList list)`
|
||||
2. Convert string args to interpolation: `list.Add(num, val.ToString())` -> `list.Add(num, $"{val}")`
|
||||
3. Apply string hole rule: `$"Text\t{val}"` -> `$"{"Text"}\t{val}"`
|
||||
4. Tab-separated: `string.Format("{0}\t{1}", a, b)` -> `$"{a}\t{b}"`
|
||||
5. Cliloc as argument: `"#" + cliloc` -> `$"{cliloc:#}"`
|
||||
|
||||
## Critical Rule: String Literals Must Be Holes
|
||||
Only `\t` should be a bare literal. All text must be inside `{}` holes:
|
||||
```csharp
|
||||
// BAD: "Map" is a delimiter
|
||||
list.Add(1060658, $"Map\t{value}");
|
||||
// GOOD: "Map" is an argument
|
||||
list.Add(1060658, $"{"Map"}\t{value}");
|
||||
```
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/06-property-lists.md` -- detailed migration reference
|
||||
- `dev-docs/property-lists.md` -- complete ModernUO property list system
|
||||
- `dev-docs/claude-skills/modernuo-property-lists.md` -- ModernUO property list skill
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
name: migrate-serialization
|
||||
description: >
|
||||
Trigger: when converting RunUO Serialize/Deserialize methods, adding [SerializableField], converting [Constructable] to [Constructible], or migrating manual serialization code.
|
||||
Covers: source-generated serialization, field conversion, version handling, TypeAlias.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Serialization Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting `Serialize(GenericWriter)`/`Deserialize(GenericReader)` overrides
|
||||
- Converting `[Constructable]` to `[Constructible]`
|
||||
- Adding `[SerializableField]` attributes
|
||||
- Handling save compatibility with `[TypeAlias]`
|
||||
|
||||
## Conversion Steps
|
||||
1. Add `using ModernUO.Serialization;`
|
||||
2. Add `[SerializationGenerator(0, false)]` to class (version 0, false for Item/Mobile)
|
||||
3. Add `partial` to class declaration
|
||||
4. Convert each serialized field: `private int m_X` -> `[SerializableField(N)] private int _x`
|
||||
5. Add `[SerializedCommandProperty(AccessLevel.X)]` if RunUO had `[CommandProperty]`
|
||||
6. Add `[InvalidateProperties]` if setter called `InvalidateProperties()`
|
||||
7. DELETE the `Serial` constructor
|
||||
8. DELETE `Serialize()` and `Deserialize()` overrides
|
||||
9. Change `[Constructable]` to `[Constructible]`
|
||||
10. Timer fields: leave unserialized, add `[AfterDeserialization]` method
|
||||
|
||||
## Quick Mapping
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `public class Foo : Item` | `[SerializationGenerator(0, false)] public partial class Foo : Item` |
|
||||
| `private int m_X` + manual Serialize | `[SerializableField(0)] private int _x` |
|
||||
| `[CommandProperty(GM)]` on property | `[SerializedCommandProperty(GM)]` on field |
|
||||
| `Foo(Serial serial) : base(serial)` | DELETE |
|
||||
| `Serialize(GenericWriter)` | DELETE -- auto-generated |
|
||||
| `Deserialize(GenericReader)` | DELETE -- auto-generated |
|
||||
| Custom setter with InvalidateProperties() | `[InvalidateProperties]` attribute |
|
||||
| Custom setter logic | `[SerializableProperty(N)]` with `this.MarkDirty()` |
|
||||
| `reader.ReadMobile()` | `reader.ReadEntity<Mobile>()` |
|
||||
| `reader.ReadItem()` | `reader.ReadEntity<Item>()` |
|
||||
|
||||
## Anti-Patterns
|
||||
- Missing `partial` keyword -> build error
|
||||
- Serializing `TimerExecutionToken` -> build error
|
||||
- Missing `this.MarkDirty()` in `[SerializableProperty]` setter -> changes not saved
|
||||
- Wrong field prefix (`m_` instead of `_`)
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/02-serialization.md` -- detailed migration reference with before/after
|
||||
- `dev-docs/serialization.md` -- complete ModernUO serialization system
|
||||
- `dev-docs/claude-skills/modernuo-serialization.md` -- ModernUO serialization skill (patterns, attributes, examples)
|
||||
45
dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md
Normal file
45
dev-docs/claude-skills/migrate-from-runuo/migrate-systems.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
name: migrate-systems
|
||||
description: >
|
||||
Trigger: when converting multi-file RunUO engines or systems (crafting, spawners, economy, quests) to ModernUO.
|
||||
Covers: system mapping, conversion order, file organization, cross-reference handling.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Multi-File System Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting RunUO systems with multiple interdependent files
|
||||
- Converting custom engines (crafting, spawners, economy, quests)
|
||||
- Organizing RunUO `Scripts/Custom/` code into ModernUO structure
|
||||
|
||||
## Conversion Order
|
||||
1. **Data types / enums** -- Just naming and namespace changes
|
||||
2. **Persistence classes** -- `EventSink.WorldSave` -> `GenericPersistence`
|
||||
3. **Core entities (Items/Mobiles)** -- Full serialization migration
|
||||
4. **Gumps** -- Convert to `DynamicGump`/`StaticGump`
|
||||
5. **Commands** -- Usually minimal changes
|
||||
6. **Packets** -- Convert to `SpanWriter` if custom packets exist
|
||||
7. **Entry point** -- Update Configure/Initialize registration
|
||||
|
||||
## File Organization
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `Scripts/Custom/MySystem/` | `Projects/UOContent/Engines/MySystem/` or `Projects/UOContent/Systems/MySystem/` |
|
||||
| `Scripts/Items/X.cs` | `Projects/UOContent/Items/{Category}/X.cs` |
|
||||
| `Scripts/Mobiles/X.cs` | `Projects/UOContent/Mobiles/{Category}/X.cs` |
|
||||
| `Scripts/Gumps/X.cs` | `Projects/UOContent/Gumps/X.cs` |
|
||||
|
||||
## Configuration Migration
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| XML config files | `ServerConfiguration.GetOrUpdateSetting()` or `JsonConfig` |
|
||||
| Custom .cfg parsing | `ServerConfiguration` for simple, `JsonConfig` for complex |
|
||||
|
||||
## Testing
|
||||
After converting: `dotnet build`, fix errors, test [add for items, verify gumps, check persistence across save/restart.
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/10-systems-engines.md` -- detailed system migration patterns
|
||||
- `dev-docs/configuration.md` -- ModernUO configuration system
|
||||
- `dev-docs/claude-skills/modernuo-configuration.md` -- ModernUO configuration skill
|
||||
- All other migrate-* skills for system-specific guidance
|
||||
44
dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md
Normal file
44
dev-docs/claude-skills/migrate-from-runuo/migrate-timers.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: migrate-timers
|
||||
description: >
|
||||
Trigger: when converting RunUO Timer subclasses, Timer.DelayCall patterns, or TimerPriority usage to ModernUO fire-and-forget timers.
|
||||
Covers: Timer subclass elimination, TimerExecutionToken, callback patterns, timer restoration.
|
||||
---
|
||||
|
||||
# RunUO -> ModernUO Timer Migration
|
||||
|
||||
## When This Activates
|
||||
- Converting nested `Timer` subclasses with `OnTick()`
|
||||
- Converting `Timer.DelayCall()` patterns
|
||||
- Removing `TimerPriority` usage
|
||||
- Restoring timers after deserialization
|
||||
|
||||
## Conversion Steps
|
||||
1. Move `OnTick()` logic to a method on the parent class
|
||||
2. Replace `new InternalTimer(this).Start()` with `Timer.StartTimer(..., callback, out _token)`
|
||||
3. Add `private TimerExecutionToken _token;` (NOT serialized)
|
||||
4. Cancel in `OnAfterDelete()`: `_token.Cancel();`
|
||||
5. Restore in `[AfterDeserialization]`: re-call `Timer.StartTimer(...)`
|
||||
6. Delete the nested Timer class entirely
|
||||
7. Remove all `TimerPriority` references
|
||||
|
||||
## Quick Mapping
|
||||
| RunUO | ModernUO |
|
||||
|---|---|
|
||||
| `new Timer(delay).Start()` | `Timer.StartTimer(delay, callback)` |
|
||||
| `new Timer(delay, interval).Start()` | `Timer.StartTimer(delay, interval, callback, out token)` |
|
||||
| `timer.Stop()` | `token.Cancel()` |
|
||||
| `Timer.DelayCall(delay, callback)` | `Timer.StartTimer(delay, callback)` |
|
||||
| `Timer.DelayCall(delay, stateCallback, state)` | `Timer.DelayCall(delay, callback, state)` |
|
||||
| `TimerPriority.XXX` | Remove -- timer wheel auto-schedules |
|
||||
| Timer started in `Deserialize()` | `[AfterDeserialization]` method |
|
||||
|
||||
## Anti-Patterns
|
||||
- Serializing `TimerExecutionToken` -- it's a struct tracking a pooled timer
|
||||
- Starting timers in `Deserialize()` -- world isn't loaded yet, use `[AfterDeserialization]`
|
||||
- Lambda closures on hot paths -- use `Timer.DelayCall` with state parameters instead
|
||||
|
||||
## See Also
|
||||
- `dev-docs/runuo-migration-docs/03-timers.md` -- detailed migration reference
|
||||
- `dev-docs/timers.md` -- complete ModernUO timer system
|
||||
- `dev-docs/claude-skills/modernuo-timers.md` -- ModernUO timer skill
|
||||
|
|
@ -221,10 +221,52 @@ public partial class BagOfSending : Item
|
|||
}
|
||||
```
|
||||
|
||||
## Custom Serialize/Deserialize (Purity Rules)
|
||||
|
||||
When writing custom `Serialize(IGenericWriter)` or `Deserialize(IGenericReader)` methods (e.g. for `GenericPersistence` subclasses), the following rules apply:
|
||||
|
||||
### Serialize() MUST remain pure
|
||||
`Serialize()` is called from **background serialization threads** during world saves (see `SerializationThreadWorker`). Multiple entities are serialized in parallel across threads. This means `Serialize()` must NOT:
|
||||
|
||||
- **Create or destroy Items/Mobiles** -- mutates shared world state
|
||||
- **Move, equip, or unequip Items/Mobiles** -- mutates shared world state
|
||||
- **Start or stop timers** (`Timer.StartTimer`, `Timer.DelayCall`, `_token.Cancel()`) -- timers are NOT thread-safe
|
||||
- **Send packets or modify NetState** -- networking is game-thread-only
|
||||
- **Access or modify other entities' mutable state** -- data race
|
||||
- **Call `Delete()`** on anything -- triggers deletion cascades on wrong thread
|
||||
|
||||
`Serialize()` should ONLY read fields and write them to the `IGenericWriter`. Treat it as a read-only snapshot.
|
||||
|
||||
```csharp
|
||||
// CORRECT -- pure reads and writes only
|
||||
public override void Serialize(IGenericWriter writer)
|
||||
{
|
||||
writer.WriteEncodedInt(0); // version
|
||||
writer.WriteEncodedInt(_records.Count);
|
||||
foreach (var (key, value) in _records)
|
||||
{
|
||||
writer.Write(key);
|
||||
writer.Write(value);
|
||||
}
|
||||
}
|
||||
|
||||
// WRONG -- side effects in Serialize
|
||||
public override void Serialize(IGenericWriter writer)
|
||||
{
|
||||
CleanupExpiredEntries(); // BAD: mutates state
|
||||
Timer.StartTimer(Recheck); // BAD: not thread-safe
|
||||
writer.Write(_data);
|
||||
}
|
||||
```
|
||||
|
||||
### Deserialize() runs on the game thread
|
||||
`Deserialize()` runs during world load on the main thread, so it CAN create entities and start timers. However, prefer `[AfterDeserialization]` for timer setup to keep deserialization clean.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- **Missing `partial`**: `[SerializationGenerator]` requires `partial class`
|
||||
- **Serializing timers**: `TimerExecutionToken` cannot be serialized
|
||||
- **Side effects in `Serialize()`**: Serialize runs on background threads -- must be pure (no creating/destroying entities, no timer start/stop, no packets)
|
||||
- **Missing `MarkDirty()`**: Custom property setters must call `this.MarkDirty()`
|
||||
- **Wrong field prefix**: Use `_camelCase`, not `m_camelCase` for new fields
|
||||
- **Forgetting `[Constructible]`**: Items/Mobiles need this for `[add` command
|
||||
|
|
|
|||
|
|
@ -116,19 +116,32 @@ using var list = PooledRefList<Mobile>.CreateMT();
|
|||
|
||||
## World Saves
|
||||
|
||||
World saves DO involve background work, but this is handled by server infrastructure:
|
||||
1. **Serialization** happens on the main thread (safe access to game state)
|
||||
2. **Disk I/O** may happen on background threads (no game state access)
|
||||
3. Main thread blocks briefly during serialization snapshot
|
||||
World saves use **parallel serialization threads**. This is critical to understand:
|
||||
|
||||
1. **Preserialize**: Allocates heaps and wakes serialization thread workers (background)
|
||||
2. **Snapshot**: Main thread calls `Persistence.SerializeAll()` which pushes entities into `SerializationThreadWorker` queues (round-robin). Workers call `Serialize(writer)` on **their own background threads** in parallel.
|
||||
3. **Write snapshot**: Disk I/O on background threads after serialization completes
|
||||
|
||||
```csharp
|
||||
// From World.cs -- save flow:
|
||||
World.Save();
|
||||
→ Preserialize() on thread pool (allocate heaps)
|
||||
→ Snapshot() on main thread (serialize game state)
|
||||
→ WriteFiles() on thread pool (disk I/O only)
|
||||
→ Preserialize() on thread pool (allocate heaps, wake serialization workers)
|
||||
→ Snapshot() on main thread (queues entities to workers, workers serialize in parallel)
|
||||
→ SerializationThreadWorker.Execute() calls e.Serialize(writer) on background thread
|
||||
→ PauseSerializationThreads() (wait for workers to finish)
|
||||
→ WriteSnapshot() on thread pool (disk I/O only)
|
||||
```
|
||||
|
||||
### Serialize() runs on background threads
|
||||
Because `SerializationThreadWorker` calls `Serialize()` on its own thread, **`Serialize()` must be pure**:
|
||||
- **NO** creating/destroying Items or Mobiles
|
||||
- **NO** starting/stopping timers (not thread-safe)
|
||||
- **NO** sending packets or modifying NetState
|
||||
- **NO** mutating shared game state
|
||||
- **ONLY** read fields and write to `IGenericWriter`
|
||||
|
||||
See `modernuo-serialization.md` for full purity rules.
|
||||
|
||||
## Exceptions: Server Infrastructure
|
||||
|
||||
These files MAY use threading (they're server infrastructure, not game logic):
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ description: >
|
|||
3. **Never serialize `TimerExecutionToken`** -- restore in `[AfterDeserialization]`
|
||||
4. **Always cancel timers in `OnDelete()`/`OnAfterDelete()`**
|
||||
5. **8ms minimum precision** -- timer wheel uses 8ms tick rate
|
||||
6. **Timers are NOT thread-safe** -- never Start/Stop timers or call `Timer.DelayCall`/`Timer.StartTimer` from any thread other than the game thread. This includes `Serialize()` which runs on background serialization threads during world saves.
|
||||
|
||||
## Preferred APIs (In Order)
|
||||
|
||||
|
|
@ -193,6 +194,7 @@ _decayTimer = null;
|
|||
- **Forgetting cleanup**: Timers keep running if not cancelled on deletion
|
||||
- **Using `Thread.Sleep`**: Blocks the game loop. Use `Timer.StartTimer` or `await Timer.Pause` instead
|
||||
- **Creating timers in constructors called during deserialization**: Use `[AfterDeserialization]` instead
|
||||
- **Starting/stopping timers in `Serialize()`**: `Serialize()` runs on background threads during world saves -- timer APIs are game-thread-only and will corrupt state or crash
|
||||
|
||||
## Real Examples
|
||||
- Token cleanup: `Projects/UOContent/Spells/Third/WallOfStone.cs`
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue