ModernUO/dev-docs/runuo-migration-docs/05-packets-networking.md
Kamron Batman 4f9bc1d9f6
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
2026-03-13 00:33:45 -07:00

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