10 KiB
10 KiB
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:
- Data types / enums — No serialization changes needed, just naming/namespace
- Persistence classes — Convert EventSink.WorldSave to GenericPersistence
- Core entities (Items/Mobiles) — Serialization, timers, properties
- Gumps — Convert to DynamicGump/StaticGump
- Commands — Usually minimal changes
- Packets — Convert to SpanWriter if custom packets exist
- 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)
// 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)
// ModernUO
namespace Server.Custom.BountySystem;
public class BountySystem : GenericPersistence
{
private static BountySystem _instance;
private static readonly List<BountyEntry> _bounties = new();
public static IReadOnlyList<BountyEntry> 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<Mobile>();
var poster = reader.ReadEntity<Mobile>();
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)
// 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)
// 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)
// 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 <amount>")]
[Description("Place a bounty on a targeted player")]
public static void Bounty_OnCommand(CommandEventArgs e)
{
if (e.Length < 1)
{
e.Mobile.SendMessage("Usage: [Bounty <amount>");
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:
- Convert shared data types first
- Convert the persistence/core system next
- Convert consumers (gumps, commands) last
- 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:
// 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<MyConfig>(configPath);
Testing the Migration
After converting all files:
- Compile the project:
dotnet build - Fix any compilation errors
- Start the server and check for runtime errors
- Test each feature:
- Items can be
[added - Gumps display correctly
- Commands work
- Data persists across saves/restarts
- Timers fire correctly
- Items can be
See Also
dev-docs/content-patterns.md— File organization patternsdev-docs/configuration.md— Configuration system reference- All prior migration docs (01 through 09)