ModernUO/dev-docs/runuo-migration-docs/07-commands-events.md
Kamron Batman 4f9bc1d9f6
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
2026-03-13 00:33:45 -07:00

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 WorldSaveEventArgsAction (no args)
EventSink.WorldLoad EventSink.WorldLoad WorldLoadEventArgsAction (no args)
EventSink.Login EventSink.Connected LoginEventArgsAction<Mobile>
EventSink.Logout EventSink.Disconnected LogoutEventArgsAction<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.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