# 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)] 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)