## 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
309 lines
9.2 KiB
Markdown
309 lines
9.2 KiB
Markdown
# Packets & Networking Migration
|
|
|
|
## Overview
|
|
|
|
RunUO uses a `Packet` class hierarchy where outgoing packets are objects with `PacketWriter`, and incoming packets use `PacketHandler` with `PacketReader`. ModernUO completely removes the `Packet` class hierarchy. Outgoing packets are static methods using `SpanWriter` with stack-allocated buffers. Incoming packets are registered function pointers using `SpanReader`.
|
|
|
|
## RunUO Outgoing Packet Pattern
|
|
|
|
```csharp
|
|
// RunUO — Packet subclass
|
|
public sealed class MyPacket : Packet
|
|
{
|
|
public MyPacket(Serial target, int value) : base(0xBF, 12)
|
|
{
|
|
m_Stream.Write((ushort)12);
|
|
m_Stream.Write((ushort)0x99);
|
|
m_Stream.Write((int)target);
|
|
m_Stream.Write((short)value);
|
|
}
|
|
}
|
|
|
|
// Usage:
|
|
ns.Send(new MyPacket(target.Serial, 42));
|
|
```
|
|
|
|
## ModernUO Outgoing Packet Pattern
|
|
|
|
```csharp
|
|
// ModernUO — Static create method + extension method
|
|
public static class OutgoingMyPackets
|
|
{
|
|
public const int MyPacketLength = 12;
|
|
|
|
public static void CreateMyPacket(Span<byte> buffer, Serial target, int value)
|
|
{
|
|
if (buffer[0] != 0)
|
|
return;
|
|
|
|
var writer = new SpanWriter(buffer);
|
|
writer.Write((byte)0xBF);
|
|
writer.Write((ushort)12);
|
|
writer.Write((ushort)0x99);
|
|
writer.Write(target);
|
|
writer.Write((short)value);
|
|
}
|
|
}
|
|
|
|
public static class MyPacketExtensions
|
|
{
|
|
public static void SendMyPacket(this NetState ns, Serial target, int value)
|
|
{
|
|
if (ns.CannotSendPackets())
|
|
return;
|
|
|
|
var buffer = stackalloc byte[OutgoingMyPackets.MyPacketLength].InitializePacket();
|
|
OutgoingMyPackets.CreateMyPacket(buffer, target, value);
|
|
ns.Send(buffer);
|
|
}
|
|
}
|
|
|
|
// Usage:
|
|
mobile.NetState.SendMyPacket(target.Serial, 42);
|
|
```
|
|
|
|
## RunUO Incoming Packet Pattern
|
|
|
|
```csharp
|
|
// RunUO — PacketHandler registration
|
|
public class MyPacketHandlers
|
|
{
|
|
public static void Initialize()
|
|
{
|
|
PacketHandlers.Register(0x99, 12, true, new OnPacketReceive(MyHandler));
|
|
}
|
|
|
|
public static void MyHandler(NetState state, PacketReader pvSrc)
|
|
{
|
|
Serial target = pvSrc.ReadInt32();
|
|
int value = pvSrc.ReadInt16();
|
|
// Process...
|
|
}
|
|
}
|
|
```
|
|
|
|
## ModernUO Incoming Packet Pattern
|
|
|
|
```csharp
|
|
// ModernUO — Function pointer registration
|
|
public static class IncomingMyPackets
|
|
{
|
|
public static unsafe void Configure()
|
|
{
|
|
IncomingPackets.Register(0x99, 12, true, &MyHandler);
|
|
}
|
|
|
|
public static void MyHandler(NetState state, SpanReader reader)
|
|
{
|
|
var target = (Serial)reader.ReadUInt32();
|
|
var value = reader.ReadInt16();
|
|
// Process...
|
|
}
|
|
}
|
|
```
|
|
|
|
## Migration Mapping Table
|
|
|
|
| RunUO | ModernUO | Notes |
|
|
|---|---|---|
|
|
| `class MyPacket : Packet` | Static `CreateXxx(Span<byte>)` method | No class hierarchy |
|
|
| `Packet(packetId, length)` constructor | `new SpanWriter(buffer)` | Stack-allocated buffer |
|
|
| `m_Stream.Write(value)` | `writer.Write(value)` | Same method names |
|
|
| `PacketWriter` | `SpanWriter` | Ref struct, stack-allocated |
|
|
| `PacketReader` | `SpanReader` | Ref struct |
|
|
| `pvSrc.ReadInt32()` | `reader.ReadInt32()` | Same names |
|
|
| `pvSrc.ReadString()` | `reader.ReadAsciiSafe()` or `reader.ReadBigUniSafe()` | Explicit encoding |
|
|
| `pvSrc.ReadUnicodeStringSafe()` | `reader.ReadBigUniSafe()` | Explicit name |
|
|
| `ns.Send(new MyPacket(...))` | `ns.SendMyPacket(...)` | Extension method |
|
|
| `PacketHandlers.Register(id, len, ingame, handler)` | `IncomingPackets.Register(id, len, ingame, &handler)` | Function pointer |
|
|
| `new OnPacketReceive(handler)` | `&handler` | Function pointer, no delegate |
|
|
| `Packet.Compile()` / `Packet.SetStatic()` | Not needed | Buffer is stack-allocated |
|
|
| `Packet.Acquire()` / `Packet.Release()` | Not needed | No pooling, stack memory |
|
|
| Variable-length: `this.EnsureCapacity(len)` | `writer.WritePacketLength()` at end | Fill length at position 1-2 |
|
|
|
|
## Step-by-Step Conversion
|
|
|
|
### Outgoing Packets
|
|
|
|
#### Step 1: Create Static Class
|
|
```csharp
|
|
public static class OutgoingMySystemPackets
|
|
{
|
|
// Constants and Create methods go here
|
|
}
|
|
```
|
|
|
|
#### Step 2: Convert Packet Constructor to Create Method
|
|
```csharp
|
|
// Determine packet length from RunUO constructor: base(0xBF, 12)
|
|
public const int MyPacketLength = 12;
|
|
|
|
public static void CreateMyPacket(Span<byte> buffer, Serial target, int value)
|
|
{
|
|
if (buffer[0] != 0) // Already initialized guard
|
|
return;
|
|
|
|
var writer = new SpanWriter(buffer);
|
|
writer.Write((byte)0xBF); // Packet ID
|
|
writer.Write((ushort)12); // Length
|
|
writer.Write((ushort)0x99); // Sub-command
|
|
writer.Write(target); // Serial
|
|
writer.Write((short)value); // Value
|
|
}
|
|
```
|
|
|
|
#### Step 3: Create Send Extension Method
|
|
```csharp
|
|
public static void SendMyPacket(this NetState ns, Serial target, int value)
|
|
{
|
|
if (ns.CannotSendPackets())
|
|
return;
|
|
|
|
var buffer = stackalloc byte[MyPacketLength].InitializePacket();
|
|
CreateMyPacket(buffer, target, value);
|
|
ns.Send(buffer);
|
|
}
|
|
```
|
|
|
|
#### Step 4: For Variable-Length Packets
|
|
```csharp
|
|
public static void SendDynamicPacket(this NetState ns, string name, int[] values)
|
|
{
|
|
if (ns.CannotSendPackets())
|
|
return;
|
|
|
|
var length = 7 + name.Length * 2 + values.Length * 4;
|
|
var writer = new SpanWriter(stackalloc byte[length]);
|
|
|
|
writer.Write((byte)0x99);
|
|
writer.Write((ushort)0); // Length placeholder
|
|
writer.WriteBigUniNull(name);
|
|
writer.Write((ushort)values.Length);
|
|
|
|
foreach (var val in values)
|
|
writer.Write(val);
|
|
|
|
writer.WritePacketLength(); // Fill in actual length
|
|
ns.Send(writer.Span);
|
|
}
|
|
```
|
|
|
|
### Incoming Packets
|
|
|
|
#### Step 1: Change Registration
|
|
```csharp
|
|
// RunUO (Initialize)
|
|
PacketHandlers.Register(0x99, 12, true, new OnPacketReceive(MyHandler));
|
|
|
|
// ModernUO (Configure, unsafe)
|
|
public static unsafe void Configure()
|
|
{
|
|
IncomingPackets.Register(0x99, 12, true, &MyHandler);
|
|
}
|
|
```
|
|
|
|
#### Step 2: Update Handler Signature
|
|
```csharp
|
|
// RunUO
|
|
public static void MyHandler(NetState state, PacketReader pvSrc)
|
|
|
|
// ModernUO
|
|
public static void MyHandler(NetState state, SpanReader reader)
|
|
```
|
|
|
|
#### Step 3: Update Read Methods
|
|
```csharp
|
|
// RunUO → ModernUO
|
|
pvSrc.ReadInt32() → reader.ReadInt32()
|
|
pvSrc.ReadInt16() → reader.ReadInt16()
|
|
pvSrc.ReadByte() → reader.ReadByte()
|
|
pvSrc.ReadBoolean() → reader.ReadBoolean()
|
|
pvSrc.ReadString() → reader.ReadAsciiSafe()
|
|
pvSrc.ReadUnicodeStringSafe() → reader.ReadBigUniSafe()
|
|
pvSrc.ReadUnicodeString() → reader.ReadBigUni()
|
|
pvSrc.Seek(offset, origin) → reader.Seek(offset, origin)
|
|
```
|
|
|
|
## SpanWriter Quick Reference
|
|
|
|
```csharp
|
|
writer.Write(bool); // 1 byte
|
|
writer.Write(byte); // 1 byte
|
|
writer.Write(short); // 2 bytes big-endian
|
|
writer.Write(ushort); // 2 bytes big-endian
|
|
writer.Write(int); // 4 bytes big-endian
|
|
writer.Write(uint); // 4 bytes big-endian
|
|
writer.Write(Serial); // 4 bytes
|
|
writer.WriteAsciiNull(str); // ASCII null-terminated
|
|
writer.WriteBigUniNull(str); // UTF-16 BE null-terminated
|
|
writer.WriteLE(int); // 4 bytes little-endian
|
|
writer.WritePacketLength(); // Fill length at pos 1-2
|
|
```
|
|
|
|
## SpanReader Quick Reference
|
|
|
|
```csharp
|
|
reader.ReadByte(); // 1 byte
|
|
reader.ReadBoolean(); // 1 byte
|
|
reader.ReadInt16(); // 2 bytes big-endian
|
|
reader.ReadUInt16(); // 2 bytes big-endian
|
|
reader.ReadInt32(); // 4 bytes big-endian
|
|
reader.ReadUInt32(); // 4 bytes big-endian
|
|
reader.ReadAsciiSafe(); // ASCII, filtered
|
|
reader.ReadBigUniSafe(); // UTF-16 BE, filtered
|
|
reader.Seek(offset, origin); // Position
|
|
reader.Remaining; // Bytes left
|
|
```
|
|
|
|
## Shared Buffer Pattern
|
|
|
|
When sending the same packet to multiple players:
|
|
```csharp
|
|
public static void SendToNearby(Mobile source, int effectId)
|
|
{
|
|
Span<byte> buffer = stackalloc byte[EffectPacketLength];
|
|
buffer.InitializePacket();
|
|
|
|
foreach (var ns in source.GetClientsInRange(18))
|
|
{
|
|
CreateEffectPacket(buffer, source.Serial, effectId);
|
|
ns.Send(buffer);
|
|
}
|
|
}
|
|
```
|
|
|
|
The `buffer[0] != 0` guard in `Create` methods prevents re-initialization, so the buffer is built once and reused.
|
|
|
|
## Edge Cases & Gotchas
|
|
|
|
### 1. Always Check CannotSendPackets()
|
|
```csharp
|
|
if (ns.CannotSendPackets())
|
|
return;
|
|
```
|
|
|
|
### 2. Use InitializePacket() on stackalloc
|
|
```csharp
|
|
var buffer = stackalloc byte[length].InitializePacket();
|
|
```
|
|
|
|
### 3. Big-Endian by Default
|
|
UO protocol is big-endian. Only use `WriteLE`/`ReadLE` when specifically needed.
|
|
|
|
### 4. Use Safe String Reads
|
|
Always use `ReadAsciiSafe()`/`ReadBigUniSafe()` for incoming strings to filter control characters.
|
|
|
|
### 5. Function Pointers Require unsafe
|
|
The `Configure()` method must be marked `unsafe` for function pointer syntax `&handler`.
|
|
|
|
### 6. Many Common Packets Already Exist
|
|
Before writing custom packet code, check if ModernUO already has a `Send*` extension method in:
|
|
- `OutgoingMobilePackets` — Mobile status, animation, movement
|
|
- `OutgoingItemPackets` — Item display, updates
|
|
- `OutgoingEffectPackets` — Effects, sounds
|
|
- `OutgoingContainerPackets` — Container contents
|
|
|
|
## See Also
|
|
|
|
- `dev-docs/networking-packets.md` — Complete ModernUO networking reference
|
|
- `01-foundation-changes.md` — Foundation changes
|