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
343
dev-docs/runuo-migration-docs/04-gumps.md
Normal file
343
dev-docs/runuo-migration-docs/04-gumps.md
Normal file
|
|
@ -0,0 +1,343 @@
|
|||
# Gump Migration
|
||||
|
||||
## Overview
|
||||
|
||||
RunUO uses a `Gump` base class where layout is built in the constructor using `AddXxx()` entry methods. ModernUO provides `StaticGump<T>` (cached layout) and `DynamicGump` (rebuilt per-instance) with ref struct builders, and enforces the empty gump rule.
|
||||
|
||||
## RunUO Pattern
|
||||
|
||||
```csharp
|
||||
using Server.Gumps;
|
||||
using Server.Network;
|
||||
|
||||
namespace Server.Gumps
|
||||
{
|
||||
public class ConfirmGump : Gump
|
||||
{
|
||||
private Mobile m_From;
|
||||
private string m_Message;
|
||||
|
||||
public ConfirmGump(Mobile from, string message) : base(50, 50)
|
||||
{
|
||||
m_From = from;
|
||||
m_Message = message;
|
||||
|
||||
Closable = true;
|
||||
Disposable = true;
|
||||
Dragable = true;
|
||||
Resizable = false;
|
||||
|
||||
AddPage(0);
|
||||
AddBackground(0, 0, 400, 300, 5054);
|
||||
AddAlphaRegion(10, 10, 380, 280);
|
||||
|
||||
AddHtml(20, 20, 360, 200, message, true, true);
|
||||
|
||||
AddButton(100, 260, 4005, 4007, 1, GumpButtonType.Reply, 0);
|
||||
AddHtmlLocalized(135, 262, 100, 20, 1011036, false, false); // OK
|
||||
|
||||
AddButton(250, 260, 4017, 4019, 0, GumpButtonType.Reply, 0);
|
||||
AddHtmlLocalized(285, 262, 100, 20, 1011012, false, false); // Cancel
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, RelayInfo info)
|
||||
{
|
||||
if (info.ButtonID == 1)
|
||||
{
|
||||
m_From.SendMessage("Confirmed!");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
from.SendGump(new ConfirmGump(from, "Are you sure?"));
|
||||
```
|
||||
|
||||
## ModernUO Equivalent (DynamicGump)
|
||||
|
||||
```csharp
|
||||
using Server.Gumps;
|
||||
|
||||
namespace Server.Gumps;
|
||||
|
||||
public class ConfirmGump : DynamicGump
|
||||
{
|
||||
private readonly Mobile _from;
|
||||
private readonly string _message;
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
private ConfirmGump(Mobile from, string message) : base(50, 50)
|
||||
{
|
||||
_from = from;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref DynamicGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
builder.AddBackground(0, 0, 400, 300, 5054);
|
||||
builder.AddAlphaRegion(10, 10, 380, 280);
|
||||
|
||||
builder.AddHtml(20, 20, 360, 200, _message, background: true, scrollbar: true);
|
||||
|
||||
builder.AddButton(100, 260, 4005, 4007, 1);
|
||||
builder.AddHtmlLocalized(135, 262, 100, 20, 1011036); // OK
|
||||
|
||||
builder.AddButton(250, 260, 4017, 4019, 0);
|
||||
builder.AddHtmlLocalized(285, 262, 100, 20, 1011012); // Cancel
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
if (info.ButtonID == 1)
|
||||
{
|
||||
_from.SendMessage("Confirmed!");
|
||||
}
|
||||
}
|
||||
|
||||
public static void DisplayTo(Mobile from, string message)
|
||||
{
|
||||
if (from?.NetState == null)
|
||||
return;
|
||||
|
||||
from.SendGump(new ConfirmGump(from, message));
|
||||
}
|
||||
}
|
||||
|
||||
// Usage:
|
||||
ConfirmGump.DisplayTo(from, "Are you sure?");
|
||||
```
|
||||
|
||||
## ModernUO Equivalent (StaticGump — for fixed layouts)
|
||||
|
||||
```csharp
|
||||
using Server.Gumps;
|
||||
|
||||
namespace Server.Gumps;
|
||||
|
||||
public class ConfirmGump : StaticGump<ConfirmGump>
|
||||
{
|
||||
private readonly Mobile _from;
|
||||
private readonly string _message;
|
||||
|
||||
public override bool Singleton => true;
|
||||
|
||||
private ConfirmGump(Mobile from, string message) : base(50, 50)
|
||||
{
|
||||
_from = from;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
protected override void BuildLayout(ref StaticGumpBuilder builder)
|
||||
{
|
||||
builder.AddPage();
|
||||
builder.AddBackground(0, 0, 400, 300, 5054);
|
||||
builder.AddAlphaRegion(10, 10, 380, 280);
|
||||
|
||||
builder.AddHtmlPlaceholder(20, 20, 360, 200, "message", true, true);
|
||||
|
||||
builder.AddButton(100, 260, 4005, 4007, 1);
|
||||
builder.AddHtmlLocalized(135, 262, 100, 20, 1011036);
|
||||
|
||||
builder.AddButton(250, 260, 4017, 4019, 0);
|
||||
builder.AddHtmlLocalized(285, 262, 100, 20, 1011012);
|
||||
}
|
||||
|
||||
protected override void BuildStrings(ref GumpStringsBuilder builder)
|
||||
{
|
||||
builder.SetStringSlot("message", _message);
|
||||
}
|
||||
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
{
|
||||
if (info.ButtonID == 1)
|
||||
_from.SendMessage("Confirmed!");
|
||||
}
|
||||
|
||||
public static void DisplayTo(Mobile from, string message)
|
||||
{
|
||||
if (from?.NetState == null)
|
||||
return;
|
||||
from.SendGump(new ConfirmGump(from, message));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Migration Mapping Table
|
||||
|
||||
| RunUO | ModernUO | Notes |
|
||||
|---|---|---|
|
||||
| `class Foo : Gump` | `class Foo : DynamicGump` or `class Foo : StaticGump<Foo>` | Choose based on layout |
|
||||
| Layout in constructor | Layout in `BuildLayout(ref builder)` | Move all AddXxx calls |
|
||||
| `AddPage(0)` | `builder.AddPage()` | 0 is default |
|
||||
| `AddBackground(...)` | `builder.AddBackground(...)` | Same args, on builder |
|
||||
| `AddButton(x, y, n, p, id, GumpButtonType.Reply, 0)` | `builder.AddButton(x, y, n, p, id)` | Simplified — Reply is default |
|
||||
| `AddButton(x, y, n, p, id, GumpButtonType.Page, p)` | `builder.AddButton(x, y, n, p, id, GumpButtonType.Page, p)` | Same for page nav |
|
||||
| `AddHtml(x, y, w, h, text, bg, scroll)` | `builder.AddHtml(x, y, w, h, text, background: bg, scrollbar: scroll)` | Named params |
|
||||
| `AddHtmlLocalized(x, y, w, h, num, bg, scroll)` | `builder.AddHtmlLocalized(x, y, w, h, num)` | Simplified |
|
||||
| `AddLabel(x, y, hue, text)` | `builder.AddLabel(x, y, hue, text)` | Same |
|
||||
| `AddTextEntry(x, y, w, h, hue, id, text)` | `builder.AddTextEntry(x, y, w, h, hue, id, text)` | Same |
|
||||
| `AddCheck(x, y, off, on, state, id)` | `builder.AddCheckbox(x, y, off, on, state, id)` | Renamed |
|
||||
| `AddRadio(x, y, off, on, state, id)` | `builder.AddRadio(x, y, off, on, state, id)` | Same |
|
||||
| `Closable = false` | `builder.SetNoClose()` | Property → method |
|
||||
| `Dragable = false` | `builder.SetNoMove()` | Property → method (note: Dragable → NoMove) |
|
||||
| `Resizable = false` | `builder.SetNoResize()` | Property → method |
|
||||
| `Disposable = false` | `builder.SetNoDispose()` | Property → method |
|
||||
| `OnResponse(NetState, RelayInfo)` | `OnResponse(NetState, in RelayInfo)` | `in` keyword added |
|
||||
| `info.TextEntries[i].Text` | `info.GetTextEntry(id)` | Direct lookup by ID |
|
||||
| `info.Switches` contains check | `info.IsSwitched(switchID)` | Direct boolean check |
|
||||
| `from.SendGump(new MyGump(...))` | `MyGump.DisplayTo(from, ...)` | Static entry point |
|
||||
| `from.CloseGump(typeof(MyGump))` | `from.CloseGump<MyGump>()` | Generic method |
|
||||
| `from.HasGump(typeof(MyGump))` | `from.HasGump<MyGump>()` | Generic method |
|
||||
|
||||
## Step-by-Step Conversion
|
||||
|
||||
### Step 1: Choose Target Type
|
||||
| If the layout... | Convert to |
|
||||
|---|---|
|
||||
| Is the same structure every time | `StaticGump<T>` (best performance) |
|
||||
| Changes based on data (loops, conditionals) | `DynamicGump` |
|
||||
| You're unsure | `DynamicGump` (simpler, still much better than legacy `Gump`) |
|
||||
|
||||
### Step 2: Change Class Declaration
|
||||
```csharp
|
||||
// RunUO
|
||||
public class MyGump : Gump
|
||||
|
||||
// ModernUO
|
||||
public class MyGump : DynamicGump
|
||||
// OR
|
||||
public class MyGump : StaticGump<MyGump>
|
||||
```
|
||||
|
||||
### Step 3: Add Singleton If Needed
|
||||
```csharp
|
||||
public override bool Singleton => true; // Only one instance per player
|
||||
```
|
||||
|
||||
### Step 4: Make Constructor Private, Add DisplayTo
|
||||
```csharp
|
||||
private MyGump(Mobile from) : base(50, 50) { _from = from; }
|
||||
|
||||
public static void DisplayTo(Mobile from)
|
||||
{
|
||||
if (from?.NetState == null)
|
||||
return;
|
||||
from.SendGump(new MyGump(from));
|
||||
}
|
||||
```
|
||||
|
||||
### Step 5: Move Layout to BuildLayout
|
||||
Move all `AddXxx()` calls from the constructor to `BuildLayout(ref DynamicGumpBuilder builder)`, prefixing each with `builder.`.
|
||||
|
||||
### Step 6: Convert Properties to Builder Methods
|
||||
```csharp
|
||||
// RunUO properties in constructor
|
||||
Closable = false;
|
||||
Dragable = false;
|
||||
|
||||
// ModernUO methods in BuildLayout
|
||||
builder.SetNoClose();
|
||||
builder.SetNoMove();
|
||||
```
|
||||
|
||||
### Step 7: Update OnResponse Signature
|
||||
```csharp
|
||||
// RunUO
|
||||
public override void OnResponse(NetState sender, RelayInfo info)
|
||||
|
||||
// ModernUO
|
||||
public override void OnResponse(NetState sender, in RelayInfo info)
|
||||
```
|
||||
|
||||
### Step 8: Update Text Entry and Switch Access
|
||||
```csharp
|
||||
// RunUO
|
||||
TextRelay relay = info.GetTextEntry(0);
|
||||
string text = relay != null ? relay.Text : "";
|
||||
|
||||
// ModernUO
|
||||
string text = info.GetTextEntry(0);
|
||||
```
|
||||
|
||||
```csharp
|
||||
// RunUO
|
||||
bool isChecked = info.IsSwitched(switchID);
|
||||
// ModernUO — same
|
||||
bool isChecked = info.IsSwitched(switchID);
|
||||
```
|
||||
|
||||
### Step 9: For StaticGump — Extract Dynamic Text
|
||||
Replace variable text with placeholders:
|
||||
```csharp
|
||||
// In BuildLayout:
|
||||
builder.AddLabelPlaceholder(x, y, hue, "playerName");
|
||||
builder.AddHtmlPlaceholder(x, y, w, h, "description", false, true);
|
||||
|
||||
// In BuildStrings:
|
||||
protected override void BuildStrings(ref GumpStringsBuilder builder)
|
||||
{
|
||||
builder.SetStringSlot("playerName", _from.Name);
|
||||
builder.SetHtmlText("description", _text, "#FFC000", 4);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 10: Update Callers
|
||||
```csharp
|
||||
// RunUO
|
||||
from.SendGump(new MyGump(from));
|
||||
from.CloseGump(typeof(MyGump));
|
||||
|
||||
// ModernUO
|
||||
MyGump.DisplayTo(from);
|
||||
from.CloseGump<MyGump>();
|
||||
```
|
||||
|
||||
## Empty Gump Rule (CRITICAL)
|
||||
|
||||
**NEVER send a gump with no visual components.** An empty gump has no close button and cannot be dismissed — it leaks on both client and server.
|
||||
|
||||
This typically happens when constructor logic short-circuits:
|
||||
```csharp
|
||||
// BAD — gump is created empty, then sent
|
||||
public MyGump(Mobile from) : base(50, 50)
|
||||
{
|
||||
if (!from.Alive)
|
||||
return; // Empty gump created!
|
||||
AddPage(0);
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
**Fix**: Use the static `DisplayTo()` pattern. Validate before constructing:
|
||||
```csharp
|
||||
private MyGump(Mobile from) : base(50, 50) { /* always builds layout */ }
|
||||
|
||||
public static void DisplayTo(Mobile from)
|
||||
{
|
||||
if (!from.Alive || from.NetState == null)
|
||||
return; // No gump created at all
|
||||
from.SendGump(new MyGump(from));
|
||||
}
|
||||
```
|
||||
|
||||
## Edge Cases & Gotchas
|
||||
|
||||
### 1. using Server.Gumps Is Required
|
||||
The extension methods `SendGump()`, `HasGump<T>()`, `FindGump<T>()`, `CloseGump<T>()` are in the `Server.Gumps` namespace. Without the using, they won't resolve.
|
||||
|
||||
### 2. StaticGump Caching
|
||||
StaticGump caches layout bytes on first use. During development, override `Cached => false` to rebuild every time, but remove before committing.
|
||||
|
||||
### 3. Button ID 0 = Close
|
||||
Button ID 0 is reserved for close/cancel. Use 1+ for action buttons.
|
||||
|
||||
### 4. GumpButtonType.Reply Is Default
|
||||
In ModernUO, `AddButton(x, y, n, p, id)` defaults to Reply type. Only specify `GumpButtonType.Page` for page navigation buttons.
|
||||
|
||||
## See Also
|
||||
|
||||
- `dev-docs/gump-system.md` — Complete ModernUO gump reference
|
||||
- `01-foundation-changes.md` — Foundation changes
|
||||
Loading…
Add table
Add a link
Reference in a new issue