354 lines
10 KiB
Markdown
354 lines
10 KiB
Markdown
# 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<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)
|
|
```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 <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:
|
|
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<MyConfig>(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)
|