Ultima Online Packets
Moongate speaks the classic Ultima Online wire protocol. Every message is a
packet: a byte stream that starts with a one-byte opcode and is decoded
into (or encoded from) a small C# record struct — incoming packets implement
IIncomingPacket<T>, outgoing ones IOutgoingPacket. This page maps the
protocol surface currently implemented for the ClassicUO 7.x client target.
| Opcode | Name | Dir | Size | Description |
|---|---|---|---|---|
0x02 |
Move Request | C → S | 7 bytes (fixed) | One step or turn, with anti-fastwalk key. |
0x06 |
Double Click | C → S | 5 bytes (fixed) | The client double-clicked an entity, identified by its serial. |
0x09 |
Single Click | C → S | 5 bytes (fixed) | The client clicked an entity, identified by its serial. |
0x11 |
Status Bar Info | S → C | Variable | The player's own status window. |
0x1B |
Login Confirm | S → C | 37 bytes (fixed) | The first packet of the enter-world burst. |
0x1D |
Delete Object | S → C | 5 bytes (fixed) | The entity is gone — stop drawing it. |
0x20 |
Draw Game Player | S → C | 19 bytes (fixed) | Positions and renders the player's own mobile. |
0x22 |
Movement Ack | S → C | 3 bytes (fixed) | Confirms the step with the client's sequence number. |
0x24 |
Draw Container | S → C | 7 bytes (fixed) | Opens the container's gump on the client. |
0x25 |
Add Item To Container | S → C | 21 bytes (fixed) | Drops one item into an already-open container gump. |
0x27 |
Lift Reject | S → C | 2 bytes (fixed) | The lift the client asked for is refused, and why. |
0x2E |
Worn Item | S → C | 15 bytes (fixed) | Draws a single item on a mobile that the client already knows about. |
0x3A |
Skill Lock Change | C → S | Variable | The client sets the up/down/lock arrow on one skill. |
0x3A |
Skills | S → C | Variable | Skill list (0x3A), in the absolute-with-caps form (type 0x02): the client's whole skill list in one go. |
0x3C |
Container Content | S → C | Variable | Every item inside a container, in one variable-length packet. |
0x4E |
Personal Light Level | S → C | 6 bytes (fixed) | The light radiating around the given mobile. |
0x4F |
Overall Light Level | S → C | 2 bytes (fixed) | 0 is full daylight, higher is darker. |
0x55 |
Login Complete | S → C | 1 bytes (fixed) | The "you are now in the world" marker that unblocks the client. |
0x5B |
Game Time | S → C | 4 bytes (fixed) | The in-world clock shown to the client. |
0x5D |
Character Select | C → S | 73 bytes (fixed) | The client picks an existing character slot to enter the world with. |
0x72 |
War Mode | S → C | 5 bytes (fixed) | Toggles the client's combat stance. |
0x73 |
Ping | C → S | 2 bytes (fixed) | The client sends this periodically with a rolling sequence byte and expects the server to echo it straight back, or it eventually drops the connection. |
0x73 |
Ping Ack | S → C | 2 bytes (fixed) | Echoes the client's keep-alive sequence byte straight back. |
0x78 |
Draw Object | S → C | Variable | Draws a mobile and its equipped items on the client. |
0x80 |
Account Login Request | C → S | 62 bytes (fixed) | Credentials for the login server. |
0x82 |
Login Denied | S → C | 2 bytes (fixed) | Rejects the login with a protocol reason code. |
0x83 |
Delete Character | C → S | 39 bytes (fixed) | The client asks to delete the character in the given slot. |
0x85 |
Character Delete Result | S → C | 2 bytes (fixed) | Why a deletion was refused. |
0x86 |
Character List Update | S → C | 304 bytes (fixed) | The account's character list after it changed, so the client can redraw the selection screen. |
0x88 |
Paperdoll | S → C | 66 bytes (fixed) | Tells the client to open the character window for a mobile. |
0x8C |
Connect To Game Server | S → C | 11 bytes (fixed) | Redirects the client to the game port with an auth key. |
0x91 |
Game Server Login | C → S | 65 bytes (fixed) | The auth key from the redirect plus the account credentials. |
0xA0 |
Select Server | C → S | 3 bytes (fixed) | The shard index the client picked from the server list. |
0xA8 |
Server List | S → C | Variable | Advertises the available shards. |
0xA9 |
Character List | S → C | Variable | The character slots followed by the starting cities, in the extended 7.0.13+ layout. |
0xB9 |
Support Features | S → C | 5 bytes (fixed) | Unlocks the client feature set at login, sent right before the character list. |
0xBC |
Season Change | S → C | 3 bytes (fixed) | Sets the client's season and optionally plays the season-change sound. |
0xBD |
Client Version | C → S | Variable | The client answers the server's version request with its build string (e.g. |
0xBF |
General Information | C → S | Variable | A multiplexed request whose meaning is chosen by a leading SubCommand (ushort). |
0xBF/0x08 |
Map Change | S → C | 6 bytes (fixed) | Switches the client to the given map. |
0xBF/0x18 |
Map Patches | S → C | 41 bytes (fixed) | Declares the static/land map-diff block counts for the four classic facets. |
0xBF/0x19 |
Stat Lock Info | S → C | 12 bytes (fixed) | The up/down/lock state of the three stats, packed two bits each into a single byte. |
0xEF |
Login Seed | C → S | 21 bytes (fixed) | Connection seed and client version, sent first by ClassicUO. |
0xF3 |
World Item | S → C | 24 bytes (fixed) | Draws an item lying in the world. |
0xF8 |
Character Creation | C → S | 106 bytes (fixed) | The new 106-byte creation packet sent by clients 7.0.16.0 and later. |
How the protocol works
- Opcodes. The first byte of every packet identifies it. The same opcode can
mean different things per direction (for example
0x73is a ping from the client and a ping acknowledgement from the server). - Fixed vs variable length. Most packets have a fixed size known from the
opcode. Variable-length packets carry their total length as a big-endian
ushortright after the opcode. - Login vs game phase. The client first talks to the login server
(account auth, shard list, server select), receives a redirect (
0x8C), then reconnects to the game server and enters the world. - The
0xBFmultiplexer. General Information (0xBF) is a container: aushortsub-command selects the actual meaning (map patches, stat locks, and many more). Moongate models each implemented sub-command as its own outgoing packet class.
How a packet is implemented
Each packet is a self-contained readonly record struct that knows how to read
or write itself, and declares its documentation family with the
[PacketDocumentation] attribute:
/// <summary>
/// Double click (0x06): the client double-clicked an entity, identified by its serial. 5 bytes fixed.
/// Whether the target is a mobile or an item is decided later from the serial's range.
/// </summary>
[PacketDocumentation(PacketFamilyType.InteractionKeepalive, Length = 5)]
public readonly record struct DoubleClickPacket(Serial Target) : IIncomingPacket<DoubleClickPacket>
{
public static byte PacketId => 0x06;
public static DoubleClickPacket Read(ref SpanReader reader)
{
reader.ReadByte(); // packet id
var target = new Serial(reader.ReadUInt32());
return new(target);
}
}
The reference pages in this section are generated from those classes by
scripts/generate-packet-docs.cs — run dotnet run scripts/generate-packet-docs.cs
from the repository root after adding or changing a packet.
Packet families
Every packet belongs to a family — click a card for the family's packet list.
Login & shard select
Seed, account auth, server list, shard select, game-server handoff.
Characters
Character list, creation, selection, deletion and list updates.
Enter world
Login confirm, feature flags, and the login-complete marker.
World state
Light levels, game time, season, map change/patches, object removal.
Items & containers
World items, worn items, container gumps, contents and lift rejects.
Movement
Move requests, acks, and mobile position updates.
Status & skills
Mobile status, paperdoll, skills, war mode, stat/skill locks.
Interaction & keepalive
Single/double click, the 0xBF request multiplexer, and ping round-trips.