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
This commit is contained in:
parent
31b22d4773
commit
4f9bc1d9f6
26 changed files with 4216 additions and 7 deletions
354
dev-docs/runuo-migration-docs/10-systems-engines.md
Normal file
354
dev-docs/runuo-migration-docs/10-systems-engines.md
Normal file
|
|
@ -0,0 +1,354 @@
|
|||
# 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, false)]
|
||||
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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue