## 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
7 KiB
Commands & Events Migration
Overview
Commands are largely similar between RunUO and ModernUO — the CommandSystem.Register() API is the same. The main changes are handler attribute conventions and that events now use Action<T> delegates instead of custom delegate types.
Command Changes
Registration (Same Pattern)
// 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
// RunUO
[Usage("MyCommand <arg>")]
[Description("Does something")]
// ModernUO — same attributes, plus optional Aliases
[Usage("MyCommand <arg>")]
[Description("Does something")]
[Aliases("mc", "mycmd")]
CommandEventArgs (Same)
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
// 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
// ModernUO — Action<T> delegates, changed event names
public static void Configure()
{
EventSink.WorldSave += OnWorldSave; // Action (no args)
EventSink.WorldLoad += OnWorldLoad; // Action (no args)
EventSink.Connected += OnConnected; // Action<Mobile> — was Login
EventSink.Disconnected += OnDisconnected; // Action<Mobile> — was Logout
EventSink.Speech += OnSpeech; // Action<SpeechEventArgs>
EventSink.Movement += OnMovement; // Action<MovementEventArgs>
EventSink.ServerStarted += OnServerStarted; // Action (no args)
EventSink.ServerCrashed += OnCrashed; // Action<ServerCrashedEventArgs>
}
private static void OnWorldSave()
{
// For persistence, use GenericPersistence instead (see 08-persistence.md)
}
private static void OnConnected(Mobile m)
{
m.SendMessage("Welcome!");
}
Event Migration Mapping
| RunUO Event | ModernUO Event | Signature Change |
|---|---|---|
EventSink.WorldSave |
EventSink.WorldSave |
WorldSaveEventArgs → Action (no args) |
EventSink.WorldLoad |
EventSink.WorldLoad |
WorldLoadEventArgs → Action (no args) |
EventSink.Login |
EventSink.Connected |
LoginEventArgs → Action<Mobile> |
EventSink.Logout |
EventSink.Disconnected |
LogoutEventArgs → Action<Mobile> |
EventSink.Speech |
EventSink.Speech |
SpeechEventArgs (mostly same) |
EventSink.Movement |
EventSink.Movement |
MovementEventArgs (mostly same) |
EventSink.ServerStarted |
EventSink.ServerStarted |
Action (no args) |
EventSink.Crashed |
EventSink.ServerCrashed |
Renamed |
EventSink.AggressiveAction |
EventSink.AggressiveAction |
AggressiveActionEventArgs |
EventSink.AccountLogin |
EventSink.AccountLogin |
AccountLoginEventArgs |
EventSink.SocketConnect |
EventSink.SocketConnect |
SocketConnectEventArgs |
EventSink.BeforeWorldSave |
Removed | Use WorldSave event directly |
EventSink.Shutdown |
EventSink.Shutdown |
Action |
EventSink.CharacterCreated |
Removed/Restructured | Check current source |
EventSink.OpenDoorMacroUsed |
Removed | Handle in movement/speech |
EventSink.PlayerDeath |
Use CodeGeneratedEvents | PlayerMobile.PlayerDeathEvent |
EventSink.CreatureDeath |
Use CodeGeneratedEvents | BaseCreature.CreatureDeathEvent |
Step-by-Step Conversion
Step 1: Change Initialize() to Configure()
// RunUO
public static void Initialize()
// ModernUO
public static void Configure()
Step 2: Remove Delegate Type Constructors
// 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
// 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.
// 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:
// Subscribing to a generated event
[OnEvent(nameof(PlayerMobile.PlayerLoginEvent))]
public static void HandlePlayerLogin(PlayerMobile player)
{
// Handle player login
}
Known generated events:
PlayerMobile.PlayerLoginEventPlayerMobile.PlayerDeathEventBaseCreature.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 referencedev-docs/commands-targeting.md— Complete command/targeting reference08-persistence.md— Converting WorldSave persistence01-foundation-changes.md— Foundation changes