Packets and handlers
Browse the interactive packet reference for every registered packet, its wire structure, direction, size, handler summary, and source file.
Moongate.Network.Packets defines wire formats independently of TCP and the game
server. PacketRegistry describes frames and decodes incoming packets;
IPacketHandler<TPacket> supplies synchronous game behavior, while
IAsyncPacketHandler<TPacket> handles packets that need I/O. These are separate
registrations. The initial built-in formats target ClassicUO 7.x.
Built-in packet coverage
Section titled “Built-in packet coverage”Lengths include the opcode and, for variable packets, the length header. Directions are relative to the server. This is the default table, not the whole UO protocol:
| Opcode | Class | Direction | Length | Default host handler |
|---|---|---|---|---|
0x53 |
PopupMessagePacket |
Outgoing | Fixed 2 | Game: sent before disconnecting when character creation is refused or fails |
0x55 |
LoginCompletePacket |
Outgoing | Fixed 1 | — |
0x73 |
PingPacket |
Both | Fixed 2 | Game: PingPacketHandler; Login: LoginRolePingPacketHandler |
0x80 |
AccountLoginPacket |
Incoming | Fixed 62 | Login: async account check, then 0xA8 list or 0x82 denial |
0x82 |
LoginDeniedPacket |
Outgoing | Fixed 2 | — |
0x8C |
ServerRedirectPacket |
Outgoing | Fixed 11 | Login: sent after a valid 0xA0, before closing the login connection |
0x91 |
GameLoginPacket |
Incoming | Fixed 65 | Game: validates and consumes the one-use handoff ticket, then sends 0xB9 and 0xA9 |
0xA0 |
ServerSelectPacket |
Incoming | Fixed 3 | Login: checks realm eligibility, issues ticket and redirects |
0xA6 |
UpdatesWindowPacket |
Outgoing | Variable, minimum 10 | Game: native Updates scroll for the MOTD, Windows-1252 text |
0xA8 |
ServerListPacket |
Outgoing | Variable, minimum 6 | — |
0xB9 |
SupportFeaturesPacket |
Outgoing | Fixed 5 | Game: sent after a valid 0x91 |
0xBD |
ClientVersionPacket |
Incoming | Variable, minimum 4 | ClientVersionPacketHandler: records the client version on the session |
0xBD |
ClientVersionRequestPacket |
Outgoing | Fixed 3 | — |
0xEF |
LoginSeedPacket |
Incoming | Fixed 21 | LoginSeedPacketHandler; on login listeners it also records the client version, which the handoff ticket carries to the game session |
The Ultima plugin adds these packets in game and standalone modes, with
RegisterIncomingPacket for the incoming ones (see
Host integration). 0xD9 is the exception: it is registered in every
mode, since the Enhanced Client sends it to the login server too:
| Opcode | Class | Direction | Length | Handler |
|---|---|---|---|---|
0x8D |
CreateCharacterEnhancedPacket |
Incoming | Variable | CreateCharacterEnhancedPacketHandler: creates and saves the character and starting items, then brings it into the world |
0xA9 |
CharacterListPacket |
Outgoing | Variable, minimum 6 | — |
0xD9 |
ClientHardwareInfoPacket |
Incoming | Fixed 268 | Game: IgnoredPacketHandler<T>; Login: LoginRoleIgnoredPacketHandler<T>. Recognised and ignored for now (Debug log) |
0xF8 |
CreateCharacterPacket |
Incoming | Fixed 106 | CreateCharacterPacketHandler: creates and saves the character and starting items, then brings it into the world |
0x5D |
PlayCharacterPacket |
Incoming | Fixed 73 | PlayCharacterPacketHandler: brings the chosen character into the world |
0x83 |
DeleteCharacterPacket |
Incoming | Fixed 39 | DeleteCharacterPacketHandler: marks the character for deletion |
0x02 |
MoveRequestPacket |
Incoming | Fixed 7 | MoveRequestPacketHandler: turns or steps the character, answered with 0x22 or 0x21 |
0x22 |
MovementAckPacket |
Outgoing | Fixed 3 | — |
0x21 |
MovementRejectPacket |
Outgoing | Fixed 8 | — |
0x77 |
MobileMovingPacket |
Outgoing | Fixed 17 | — |
0x06 |
UseRequestPacket |
Incoming | Fixed 5 | UseRequestPacketHandler: opens a container the character carries, or a paperdoll; rides a creature that is a mount, and gets off when the character double clicks itself |
0x03 |
AsciiSpeechRequestPacket |
Incoming | Variable, minimum 9 | SpeechRequestPacketHandler: local say or in-game dot command |
0xAD |
UnicodeSpeechRequestPacket |
Incoming | Variable, minimum 14 | SpeechRequestPacketHandler: Unicode and encoded-keyword say or dot command; the types guild (0x0D) and alliance (0x0E) go to the guild chat |
0xAE |
UnicodeSpeechMessagePacket |
Outgoing | Variable, minimum 50 | Player speech and private command output |
0x24 |
DisplayContainerPacket |
Outgoing | 7, or 9 from client 7.0.9.0 (no length header) | — |
0x3C |
ContainerContentPacket |
Outgoing | Variable, minimum 5 | — |
0x09 |
LookRequestPacket |
Incoming | Fixed 5 | LookRequestPacketHandler: shows the name over the object (0xC1) |
0x34 |
MobileQueryPacket |
Incoming | Fixed 10 | MobileQueryPacketHandler: answers the status of the character or of a mobile in sight (0x11) and the character’s skills (0x3A) |
0x75 |
RenameRequestPacket |
Incoming | Fixed 35 | RenameRequestPacketHandler: the owner renames its pet from the status bar (1 to 16 letters, no banned word), staff any creature they reach |
0x6F |
SecureTradeRequestPacket |
Incoming | Variable, minimum 8 | SecureTradeRequestPacketHandler: the player cancels the trade of its box or ticks its side; the gold command (3) and unknown commands are read and ignored |
0x6F |
SecureTradePacket |
Outgoing | 47 (display), 17 (close, update) | — |
0x72 |
WarModeRequestPacket |
Incoming | Fixed 5 | WarModeRequestPacketHandler: puts the character in war or peace mode, answers with 0x72 and shows the stance to the players around (0x77) |
0xC8 |
UpdateRangePacket |
Incoming | Fixed 2 | UpdateRangePacketHandler: answers with the server’s view range |
0xC8 |
ViewRangePacket |
Outgoing | Fixed 2 | — |
0x88 |
DisplayPaperdollPacket |
Outgoing | Fixed 66 | — |
0x07 |
LiftRequestPacket |
Incoming | Fixed 7 | LiftRequestPacketHandler: picks up an item the character carries or wears, or one on the ground within 2 tiles |
0x08 |
DropRequestPacket |
Incoming | Fixed 15 | DropRequestPacketHandler: drops the held item into a carried container or on the ground |
0x25 |
ContainerItemUpdatePacket |
Outgoing | 20, or 21 from client 6.0.1.7 (no length header) | — |
0x27 |
LiftRejectPacket |
Outgoing | Fixed 2 | — |
0x1D |
RemoveEntityPacket |
Outgoing | Fixed 5 | — |
0x1A |
WorldItemPacket |
Outgoing | Variable, minimum 16 | — |
0xF3 |
WorldItemSaPacket |
Outgoing | 24, or 26 from client 7.0.9.0 | — |
0x13 |
EquipRequestPacket |
Incoming | Fixed 10 | EquipRequestPacketHandler: puts the held item on the character, or bounces it back |
0x2E |
WornItemPacket |
Outgoing | Fixed 15 | — |
0x6E |
MobileAnimationPacket |
Outgoing | Fixed 14 | — |
0xAF |
DeathAnimationPacket |
Outgoing | Fixed 13 | — |
0x2C |
DeathStatusPacket |
Outgoing | Fixed 2 | — |
0x89 |
CorpseEquipmentPacket |
Outgoing | Variable | — |
0x74 |
VendorBuyListPacket |
Outgoing | Variable | The prices and names of a vendor’s shop window |
0x3B |
VendorEndPacket |
Outgoing | Fixed 8 | Closes the shop window |
0x3B |
VendorBuyReplyPacket |
Incoming | Variable, minimum 8 | VendorBuyReplyPacketHandler: the purchase of the shop window |
0x9E |
VendorSellListPacket |
Outgoing | Variable | What a vendor offers to buy from the player |
0x9F |
VendorSellReplyPacket |
Incoming | Variable, minimum 9 | VendorSellReplyPacketHandler: the sale of the sell list |
0x6C |
TargetCursorPacket |
Outgoing | Fixed 19 | — |
0x6C |
TargetResponsePacket |
Incoming | Fixed 19 | TargetResponsePacketHandler: completes the player’s pending target |
0x95 |
HuePickerPacket |
Outgoing | Fixed 9 | — |
0x95 |
HuePickerResponsePacket |
Incoming | Fixed 9 | HuePickerResponsePacketHandler: gives the hue picked to the player’s open picker |
0x05 |
AttackRequestPacket |
Incoming | Fixed 5 | AttackRequestPacketHandler: the character fights the mobile; a refused request is answered with 0xAA and zero |
0x22, 0xFB |
ResynchronizeRequestPacket, PublicHouseContentPacket |
Incoming | Fixed 3, 2 | IgnoredPacketHandler<T>: recognised and ignored for now (Debug log) |
0xB5 |
OpenChatWindowPacket |
Incoming | Fixed 64 | OpenChatWindowPacketHandler: the chat button is answered with the chats that work; the legacy chat window is not built |
0x9B |
HelpRequestPacket |
Incoming | Fixed 258 | HelpRequestPacketHandler: opens the help menu of the character |
0xAA |
CombatantPacket |
Outgoing | Fixed 5 | — |
0x2F |
SwingPacket |
Outgoing | Fixed 10 | — |
0x0B |
DamagePacket |
Outgoing | Fixed 7 | — |
0x12 |
TextCommandPacket |
Incoming | Variable | TextCommandPacketHandler: kind 0x24 uses the skill whose number starts the text; the other kinds are ignored for now (Debug log) |
0xB8 |
ProfileRequestPacket |
Incoming | Variable | ProfileRequestPacketHandler: mode 0 answers the profile of a character when it may be read, mode 1 edits the sender’s own profile |
0xB8 |
ProfilePacket |
Outgoing | Variable | — |
0xE1 |
ClientTypePacket |
Incoming | Variable | IgnoredPacketHandler<T>: recognised and ignored for now (Debug log) |
0xF0 |
ProtocolExtensionPacket |
Incoming | Variable, minimum 4 | ProtocolExtensionPacketHandler: command 0 answers the places of the party members out of view; the guild command is ignored |
0xBF |
ExtendedCommandPacket |
Incoming | Variable | ExtendedCommandPacketHandler: subcommand 0x06 runs a party command, 0x10 answers a tooltip, 0x1A sets the lock of a stat, 0x13 asks for the context menu of a mobile or an item, 0x15 chooses one of its entries and 0x1C casts a spell from a book or a scroll; the others are ignored for now |
0xD7 |
AbilityRequestPacket |
Incoming | Variable, minimum 9 | AbilityRequestPacketHandler: subcommand 0x19 selects a weapon special move (0 clears), subcommand 0x32 is the Quest button of the paperdoll and opens the quest log; any other subcommand is ignored, read leniently |
0xD6 |
QueryPropertiesPacket |
Incoming | Variable, at most 500 serials | QueryPropertiesPacketHandler: one 0xD6 per object the character sees |
0xD6 |
PropertyListPacket |
Outgoing | Variable | — |
0xDC |
PropertyListInfoPacket |
Outgoing | Fixed 9 | — |
0xC1 |
LocalizedMessagePacket |
Outgoing | Variable | — |
0xC2 |
TextPromptPacket |
Outgoing | Variable (21) | — |
0xC2 |
TextPromptResponsePacket |
Incoming | Variable | TextPromptResponsePacketHandler: completes the player’s pending text prompt |
0x54 |
PlaySoundPacket |
Outgoing | Fixed 12 | — |
0xC0 |
HuedEffectPacket |
Outgoing | Fixed 36 | — |
0xC7 |
ParticleEffectPacket |
Outgoing | Fixed 49 | — |
0x1B |
LoginConfirmPacket |
Outgoing | Fixed 37 | — |
0xBF |
MapChangePacket |
Outgoing | Variable, 6 (subcommand 0x08) |
— |
0xBF |
StatLockInfoPacket |
Outgoing | Variable, 12 (subcommand 0x19) |
— |
0xBF |
ClearAbilityPacket |
Outgoing | Variable, 5 (subcommand 0x21) |
— |
0xBF |
DisplayContextMenuPacket |
Outgoing | Variable, minimum 12 (subcommand 0x14) |
— |
0xBF |
PartyMemberListPacket |
Outgoing | Variable, minimum 7 (subcommand 0x06, command 0x01) |
— |
0xBF |
PartyRemoveMemberPacket |
Outgoing | Variable, minimum 11 (subcommand 0x06, command 0x02) |
— |
0xBF |
PartyMessagePacket |
Outgoing | Variable, minimum 12 (subcommand 0x06, commands 0x03 and 0x04) |
— |
0xBF |
PartyInvitePacket |
Outgoing | Variable, 10 (subcommand 0x06, command 0x07) |
— |
0xF0 |
PartyLocationsPacket |
Outgoing | Variable, minimum 8 (command 0x01) |
— |
0xD4 |
BookHeaderPacket |
Outgoing | Variable, minimum 17 | — |
0x66 |
BookPagesPacket |
Outgoing | Variable, minimum 9 | — |
0x66 |
BookPagesRequestPacket |
Incoming | Variable, minimum 7 | BookEditPacketHandler: a page request or an edit of a writable book |
0xD4 |
BookHeaderChangePacket |
Incoming | Variable, minimum 7 | BookEditPacketHandler: changes title and author of a writable book |
0x93 |
OldBookHeaderChangePacket |
Incoming | Fixed 99 | BookEditPacketHandler: the same change from clients that use the old packet |
0x71 |
BulletinBoardDisplayPacket |
Outgoing | Variable, minimum 38 | — |
0x71 |
BulletinBoardSummaryPacket |
Outgoing | Variable, minimum 22 | — |
0x71 |
BulletinBoardMessagePacket |
Outgoing | Variable, minimum 24 | — |
0x71 |
BulletinBoardRequestPacket |
Incoming | Variable, minimum 12 | BulletinBoardRequestPacketHandler: see Bulletin boards |
0xCC |
LocalizedMessageAffixPacket |
Outgoing | Variable, minimum 52 | — |
0x38 |
PathfindPacket |
Outgoing | Fixed 7 | — |
0xBC |
SeasonChangePacket |
Outgoing | Fixed 3 | — |
0xBA |
QuestArrowPacket |
Outgoing | 6 bytes, 10 from client 7.0.9.0 (no length field; marked Variable for the two sizes) | ArrowModule: arrow.show and arrow.clear; the Tracking skill; not tried with a real client |
0x4F |
GlobalLightLevelPacket |
Outgoing | Fixed 2 | — |
0x4E |
PersonalLightLevelPacket |
Outgoing | Fixed 6 | — |
0x20 |
MobileUpdatePacket |
Outgoing | Fixed 19 | — |
0x78 |
MobileIncomingPacket |
Outgoing | Variable, minimum 23 | — |
0x11 |
MobileStatusPacket |
Outgoing | Variable, 121 (version 6, a client from 7.0.30 while the resistances are on: the maximum resistances and the bonuses of the AOS era), 91 (version 5), or 43 (version 0) for another mobile | — |
0xA1, 0xA2, 0xA3 |
MobileHitsPacket, MobileManaPacket, MobileStaminaPacket |
Outgoing | Fixed 9 | Sent by MobileStateService; the hits go to the players around as a share of 100 |
0x3A |
SkillsPacket |
Outgoing | Variable, minimum 6 | The whole skill list, or one skill that changed |
0x3A |
SkillLockPacket |
Incoming | Variable, minimum 6 | SkillLockPacketHandler: sets the lock of a skill of the character (up, down or locked); a skill or lock that does not exist is ignored |
0x72 |
WarModePacket |
Outgoing | Fixed 5 | — |
0x5B |
CurrentTimePacket |
Outgoing | Fixed 4 | — |
0x65 |
WeatherPacket |
Outgoing | Fixed 4 | — |
0x6D |
PlayMusicPacket |
Outgoing | Fixed 3 | — |
0x86 |
CharacterListUpdatePacket |
Outgoing | Variable, minimum 4 | — |
0x85 |
CharacterDeleteResultPacket |
Outgoing | Fixed 2 | — |
0xB0 |
GumpPacket |
Outgoing | Variable, minimum 23 | — |
0xDD |
CompressedGumpPacket |
Outgoing | Variable, minimum 35 | — |
0xB1 |
GumpResponsePacket |
Incoming | Variable, minimum 23 | GumpResponsePacketHandler: hands the checked answer to the gump the player was sent |
0xBF |
CloseGumpPacket |
Outgoing | Variable, 13 (subcommand 0x04) |
— |
Speech reaches the speaker and the other player characters on the same map within
its range, as in ModernUO: 15 tiles aloud or as an emote, 1 tile for a whisper, 18
for a yell. The client chooses the way from what the player types (; whispers,
! yells, : emotes) and the text goes out unchanged, with its type. Global chat
and the separate chat window are not supported yet. A leading . invokes the existing command system
privately; .. escapes one dot. Empty or over-128-character speech is ignored.
0xAD is read leniently, as in the other emulators, since a refused packet disconnects the
client: a missing terminator is accepted, a badly encoded character becomes U+FFFD, and a
language code that is not three ASCII letters is read as English. Only a truncated keyword list
is refused.
A gump goes out as 0xDD (compressed) to clients from 5.0.0a and as 0xB0 to older ones; the
answer comes back as 0xB1, and the server closes a gump with 0xBF subcommand 0x04. See
Gumps.
A graphic effect goes out as 0xC0 (36 bytes): the kind (0 moving, 1 lightning, 2 fixed at a
point, 3 fixed on an object), the two serials, the graphic, both points, speed, duration, the
two flags, the hue and the render mode. The plain 0x70 is never sent, as in ModernUO. An
effect with a particle id goes to the Enhanced Client as 0xC7 (49 bytes), the same body
followed by the particle fields; every other client gets the 0xC0, or nothing when the effect
has no graphic. EffectService sends them to the players of the map within the view range of
the effect, and for a moving effect also within range of its destination, once each.
When a character enters the world the server sends, in this order (ModernUO’s, checked against
ServUO, UOX3, POL and Source-X): 0x1B login confirm, 0xBF subcommand 0x08 map, 0xBC
season, 0x4F and 0x4E light, 0x20 the player, 0x78 the player with worn items, hair and
beard (virtual serials from IMobileService), 0x11 status (version 5, or 6 for a client from 7.0.30 while the resistances are on), 0x72 peace, 0x55
login complete and 0x5B time; then CharacterEnteredWorldEvent is published. The outgoing
classes live in Moongate.Server.Ultima.Packets.World. The character is then a live MobileEntity
in IMobileService, facing the direction it was saved with.
A step (0x02) carries the direction, a running bit (0x80) and a sequence. The sequence is 0
after login or a refused step, then 1 to 255 and round again from 1; any other value is refused.
A direction the character does not face only turns it and uses no time. A step in the faced
direction books the next one 400 ms later walking or 200 ms running, and may come up to 200 ms
early; an earlier step, or one IMovementService blocks, is refused. 0x22 accepts the step with
the character’s notoriety; 0x21 refuses it and puts the client back at the real position.
Players see each other within the view range (18 tiles by default) on both axes. ISectorService keeps the live mobiles in
16×16 sectors per map, and IWorldViewService recomputes what changed from the old and new
position of each step, as ModernUO and POL do. After the enter-world sequence the player gets
0x78 of everyone in range and they get its 0x78. An accepted step or turn sends 0x77 (bit
0x80 of the direction marks a run) to the players that already saw the mover, 0x78 both ways
to the players that just came into range, and 0x1D to the players that lost it; the mover’s
client drops what it walks away from by itself. When a character leaves the world the players in
range get its 0x1D. The range is ultima.world.view_range (default 18, from 5 to 24); the client’s
0xC8 request is answered with it, whatever the client asked, so both sides use the same range.
ISectorService.Query returns the players, the NPCs and the ground items around a point, in the
view range unless another is given. A sector is active while a player stands within two sectors of it (the 5×5
sectors around each player, as ModernUO); NPCs do not wake sectors. ISectorService.IsActive tells
it, for the NPC AI to come.
When the session closes, CharacterLeaveWorldService (an ISessionClosedListener) copies the
character, removes it from the world, saves the copy and publishes CharacterLeftWorldEvent. The
world save writes the live characters too, so a crash loses at most the steps since the last save.
The items a character wears, and everything inside them at any depth, are loaded at 0x5D and
live in IItemService while it plays; they leave the world, and are saved after the character,
when its session closes, and the world save writes them too. A double click (0x06) on a live
item opens it when its graphic has the tiledata Container flag and the session’s character
carries it, worn or inside something worn: 0x24 with the gump from containers.toml (the
default entry when the graphic has none), then 0x3C with its direct contents, even when empty.
Clients before 7.0.9.0 get the 7-byte 0x24, and before 6.0.1.7 a 0x3C without the grid byte;
an unknown version gets the modern formats.
The grid byte of 0x3C and 0x25 is the item’s slot (0 to 124) in the grid the Enhanced Client
shows a container as; the classic client ignores it and uses the gump position. Every item gets a
free slot when it enters a container, and keeps it in the database (grid_index). A drop (0x08)
carries the slot the Enhanced Client asks for: the item takes it when it is free, otherwise the
next free one. A container with more than 125 items shares slots.
A double click on a mobile opens its paperdoll (0x88) when its body is human in
data/bodies.toml (a monster has none) and it is on the character’s map within
ultima.world.view_range along X and Y; the character’s own paperdoll button sends its serial
with the high bit 0x80000000 set and opens without the range check (the body must still be
human). A body missing from bodies.toml has no paperdoll. The title is built as ModernUO’s: the fame
and karma prefix of titles.toml, whose rows from 10,000 fame say Lord
or Lady, the name, then , <title> when the mobile has one (NPC templates give titles such as “the
mage”), as “The Glorious Lord Aria, the mage”; players and human NPCs alike. Skill titles are not
added yet. The flags
say whether the mobile is in war mode and whether the viewer may take items off, set only on the
character’s own paperdoll. The worn items are already known to the client from 0x78. Other
double clicks are not handled yet.
Picking an item up (0x07) records it as held in the session; it stays in its container until
the drop. A whole item inside a container the character carries, or one the character wears
(from the paperdoll; not the backpack), can be picked up: holding another item already
(AreHolding), part of a non-stackable item or of a worn stack, the backpack or an item not
carried is refused with 0x27 (CannotLift); an item of the character’s inside a container is shown back with 0x25, and a
worn one is put back on the paperdoll with 0x2E, as ModernUO; another player’s item is never
shown. A worn item picked up stays on the character until the drop, and the other players in
range see it taken off (0x1D). Dropping it (0x08)
puts it into a carried container at the drop position, brought inside the gump bounds, or at a
random spot when dropped on the container’s icon; dropped on a carried item that is not a
container, it goes into that item’s container at that item’s position. Mobiles, items not
carried, and a container into itself or anything inside it bounce the item back. Every drop frees
the hand and sends 0x25 with the item’s real position, or shows a ground item where it lies.
Lifting part of a stackable item (tiledata Generic) splits it: the held part keeps the serial,
since the client drags it, and the rest takes a serial from IItemSerialPool (64 serials reserved
from the items sequence, refilled below 16), stays where the stack was and is shown with 0x25.
With no reserved serial left the lift is refused with Inspecific. Dropping the held item onto a
carried stack of the same kind (graphic, hue, template, name and rarity, neither with props), up
to 60000, merges them: the stack grows (0x25), the
held item is removed from the client (0x1D) and from memory, and its row is deleted by the next
world save in the same transaction as the grown stack, or by the leave save, which takes over the
character’s pending deletions and drops them if it fails (the database then still holds both
stacks as before the merge). Dropping onto a container never merges. 0x5D waits for the leave
saves of the account’s last session before it loads the character, so it never reads rows older
than what that session saved. A held item dropped on a paperdoll (0x13) is worn when the paperdoll is the character’s own and
IEquipmentService allows it: the item’s own layer (the template’s, else tiledata’s for a
wearable graphic; the layer the client suggests is ignored, as ModernUO) must be one worn from
the paperdoll (not backpack, hair, beard, mount, shop or bank), free, and fit both hands as
ModernUO and POL: a template with two_handed_weapon = true needs both hands free, and nothing
else goes in hand while one is worn; shields and torches go with a one-handed weapon. The item
leaves its container or the ground and everyone in range, the character included, gets 0x2E.
Anything else bounces back to where the item still is: its container (0x25), the ground, or
the character it was taken off (0x2E to everyone in range). A stack of more than one is not
worn. The hand is always freed, and a drop or equip that names another item than the held one
puts the held one back. To keep the unique (mobile_id, layer) index satisfied after a change of
clothes, the world save and the leave save delete removed rows first and write unworn items
before worn ones, and a leave save skips an item the character dropped that someone else now
carries or wears (that owner’s save writes it).
Items on the ground live in the sector grid with the mobiles. Dropping the held item on the
ground (0x08 with destination 0xFFFFFFFF) works within 2 tiles of the character, in line of
sight from its eyes: the client’s Z is ignored and the item lands on the highest surface of the
land or a static up to 16 above the character’s feet, as ModernUO; other ground items are not
stacked on, so two items on one tile overlap. It is shown to everyone in range, the dropper
included: 0x1A for clients before 7.0.0.0, 0xF3 after, two bytes longer from 7.0.9.0.
Anyone can pick up (0x07) a ground item within 2 tiles in line of sight; it leaves every screen
(0x1D) while held, and a partial lift leaves the rest on the ground with a new serial. Too far
or out of sight is refused with OutOfRange or OutOfSight, and the item is shown again to that
player only; an item someone else holds is never shown. A held
ground item that bounces goes back where it lay, and one still held when the session closes is
put back too. Dropping onto a ground stack within reach merges them as in the backpack. Players
walking into range of a ground item, or entering the world near it, get it with the same old and
new position test as the mobiles. The items on the ground and everything inside them are loaded
at startup and saved by the world save. A ground item decays after its template’s time, 60 minutes
unless decay_minutes says otherwise, counted from when it landed on the ground and restarted
each time it is put down again; a decayed container takes its contents with it (see
Templates). A container lying on the ground, or inside one, opens within reach of the
character (0x24 and 0x3C), and 0x25 shows the items that go in and out to everyone around.
IPromptService asks a player for a line of text with the Unicode prompt (0xC2) and hands what it
typed to a callback on the game loop. A player has one prompt at a time: a new one ends the old with
no text, and so do Cancel and a closing session. The client has no packet that closes its prompt, so
a late answer is ignored: ids count up per session and an answer with another id does nothing. As
ModernUO, type 0 is the player’s escape and a text over 128 characters is ignored; the text is read up
to its terminator, without control characters, and trimmed.
ITargetService shows a player the target cursor (0x6C) and hands the pick to a callback on the
game loop, or to a command awaiting RequestAsync. A player has one target at a time: a new one
ends the old as overridden, Cancel sends 0x6C with flags 3 and id 0, and a closing session ends
it as disconnected. Cursor ids count up per session, and a response with another id is ignored.
The response is resolved as ModernUO does: x and y -1 with no serial is the player’s cancel; a
serial must be a live item or mobile; the ground takes the map’s average height, not the client’s;
a static must really be there and gives its top (half the height of a bridge). Range and line of
sight are left to the caller. In-game commands run detached from the speech packet, so a command
waiting for a target does not hold back the session’s packets; its output arrives when it ends.
A player runs one command at a time: another one meanwhile is refused with “A command is already
running.”
.where (game masters) prints what a target picks.
IHuePickerService shows a player the client’s hue picker (0x95) with a graphic in it and hands
the hue picked to a callback on the game loop. A player has one picker at a time: a new one ends the
old with no hue, and so does a closing session. Picker ids count up per session and are not the serial of an
item, as in ModernUO: an answer with another id, or with no picker open, is ignored, so a
client cannot recolour anything by sending 0x95 on its own (UOX3 recolours whatever serial the
packet names). The hue is masked with 0x3FFF and kept from 2 to 1001, as ModernUO’s
ClipDyedHue. A client that closes the picker sends nothing, so the callback may never run: whoever
opens a picker checks again, in the callback, what was true when it opened.
IDeathService kills an NPC (Death and resurrection). The players who see it are sent, in
this order, the corpse (0xF3, or 0x1A to an old client), the death (0xAF: the mobile, its
corpse, four zero bytes, as ModernUO) and, when the NPC is removed, 0x1D. The client plays the
death animation of the body by itself; the server names none. A corpse is the graphic 0x2006 and
tells the client its body in the place of the amount and the way it lies in the light byte, as
ServUO does: the item itself keeps amount 1, and both come from its props corpse.body and
corpse.direction. After the corpse of a human, elf or gargoyle body come 0x3C, with the worn
items still inside it plus its hair and beard under virtual serials, and 0x89, which names the
layer of each as the layer plus one and ends with a zero byte: the client draws the corpse dressed. Such a
body gets no 0xAF: it is told to play its fall (0x6E, action 21) and its corpse comes 1.5 seconds
later, because ClassicUO deletes the worn items of a mobile that dies by 0xAF and shows it falling
naked.
The same opcode can have different definitions in each direction, as with 0xBD.
The realm list is filtered by the authenticated account’s minimum realm level.
It contains each realm’s IPv4 address but no port. 0xA0 selects a live eligible
realm and 0x8C supplies its IPv4 address, port and one-use key. The login sender
flushes 0x8C before closing the login connection. On the new game connection
the client sends that key as a raw four-byte seed, followed by 0x91 with the
same key, username and password. The game checks the seed and atomically consumes
the Redis ticket. The ticket expires 30 seconds after it is issued, so the game
reconnect must complete within that time. A direct 0xEF client-version seed
still works on game listeners.
After a valid 0x91 the game server copies the client version from the ticket to
the session, turns on Huffman compression for everything it sends from then on,
and sends 0xB9 followed by 0xA9: the account’s saved characters in the
configured number of slots (ultima.characters.max_per_account, default 7), plus the
starting cities from data/starting_cities.toml. The creation handlers save a new
character and its starting items in one transaction. 0x5D brings the chosen
character into the world (see below).
TryGetDescriptor(opCode, out descriptor) prefers incoming, then outgoing;
descriptor.PacketType.Name gives its class name. The overload accepting
PacketDirection selects one direction explicitly when needed.
registry.TryDecode(bytes, out packet, out opCode) always sets opCode to the
first byte, even on failure. Empty input returns false with opcode zero; inspect
input length to distinguish that case. Decoding needs one complete incoming
frame, including its header. It does not buffer a TCP stream. Outgoing-only
packets have metadata but no incoming parser.
Tooltips
Section titled “Tooltips”The character list flags (0xA9) include AOS (0x20), so the client uses AOS tooltips
(object property lists) and asks for them with 0xD6 (a list of serials, at most 500; a
length that is not whole serials, or more serials, cannot be read and closes the connection, as
any unreadable packet) or 0xBF subcommand 0x10 (one serial). The
server answers one 0xD6 per object the character can see: an item it carries or wears, an
item worn by a mobile or lying on the ground within ultima.world.view_range, or a mobile in
that range on its map; anything else gets nothing. ITooltipService builds the lines, as
ModernUO and UOX3:
- an item’s name: the client’s cliloc for its graphic (1020000 + graphic, 1078872 + graphic
from
0x4000), which the client shows in its own language, or the item’s or template’s name as text; an item without a name of its own that has thelabel_numberprop shows that cliloc instead (a sign); a stack uses 1050039 with the amount; - the combat lines, clilocs the client translates, read from the item’s template, after the name and the
exceptional, crafted by and uses left lines: for a weapon (
damage_maxabove 0) 1061168, “weapon damage”, with the least and the most damage (min\tmax), and 1061167, “weapon speed”, with the rawspeedof the template (the classic number,35for a longsword, not seconds; a weapon with no speed shows 30, the fists’ speed, which is what a swing uses); for a piece of armor 1060448, “physical resist”, with itsarmor_rating(the number the status gump shows for armor before AOS, with the piece’s quality: 8 more for exceptional, 8 less for low, and none for a shield, which combat does not count yet); and for any item with astrength_requiredabove 0 1061170, “strength requirement” (shown, not enforced yet). The lines are the base numbers of the template, not the damage a hit does with the wielder’s skills. Durability, range and the hands are not shown: items have no hit points of their own yet; - in the server language, from the message files (
ILocalizationService, as UOX3): blessed or newbied (9055 “[Blessed]”) or cursed (30005), the item’s loot type else the template’s; the weight of the whole stack (30006 / 30007); the rarity (30000–30004), coloured (common white). A text missing from the files falls back to English; - a mobile: 1050045 with its name and title.
Names (items’ clilocs, amounts, mobiles’ names and titles) stay the client’s until the server has translated names; every other line is the server’s, so the tooltips follow the server language.
Free text goes through the clilocs whose whole text is ~1_NOTHING~ (1042971, 1070722, …),
one per line; an argument is cut at 504 characters, which older clients cannot exceed. The
tooltip’s revision is a 26-bit hash of its lines, as ModernUO: 0xD6 carries it, and 0xDC
carries it with bit 30 set. As ModernUO, what is shown is followed by its 0xDC: a
mobile coming into view (0x78) with each of its worn items, a ground item (0x1A/0xF3), an
item put on (0x2E), each item of an opened container (0x3C), and a container item updated
(0x25) after a split, a merge, a placement or a bounce; the client asks again when a revision
changes. The character’s own 0x78 at world entry is not followed yet: the client asks for
tooltips it does not have when the cursor is over them. A tooltip depends only on a few fields of its
object (for an item: template, graphic, amount, name, rarity, loot type, movable, label_number, worth, quality,
crafter and uses left; for a mobile: name and title), so it is cached by them: equal objects share one tooltip, and a change gives
another key, so nothing is ever invalidated. The cache keeps up to 10000 tooltips and starts
over when full. A single click (0x09) shows the first line, the name, over the
object with 0xC1.
Define a packet and test its bytes
Section titled “Define a packet and test its bytes”This example uses a private illustrative opcode. It is not an addition to the
ClassicUO protocol; use it only with a matching test client. Reference
Moongate.Network.Packets and place this in ExamplePacket.cs:
using System.Diagnostics.CodeAnalysis;using Moongate.Network.Packets.Attributes;using Moongate.Network.Packets.Base;using Moongate.Network.Packets.Interfaces;using Moongate.Network.Packets.Spans;using Moongate.Network.Packets.Types.Packets;
[PacketHandler(0xFE, PacketSizing.Fixed, Length = 3)]public sealed class ExamplePacket : BaseFixedPacket<ExamplePacket>, IIncomingPacket<ExamplePacket>, IOutgoingPacket{ public ushort Value { get; }
public ExamplePacket(ushort value) { Value = value; }
public static bool TryParse(ReadOnlySpan<byte> data, [NotNullWhen(true)] out ExamplePacket? packet) { packet = null; if (!HasValidHeader(data)) { return false; } var reader = new PacketReader(data[1..]); if (!reader.TryReadUInt16BigEndian(out var value)) { return false; } packet = new ExamplePacket(value); return true; }
public void Write(ref PacketWriter writer) { writer.EnsureCapacity(Length); writer.WriteByte(OpCode); writer.WriteUInt16BigEndian(Value); }}The metadata attribute is named PacketHandler, but describes the wire packet;
it does not register the game handler class. Call RegisterIncoming<T>() for a
packet that implements IIncomingPacket<T> (a bidirectional packet covers both
directions in one call), or RegisterOutgoing<T>() for one that implements only
IOutgoingPacket. Duplicate types or conflicting opcode/direction pairs fail
registration. Freeze only after composing the table.
Run this Program.cs as a byte-level smoke test without a server or client:
using Moongate.Network.Packets.Registry;using Moongate.Network.Packets.Serialization;
var registry = new PacketRegistry();PacketTable.Register(registry); // Optional: include the built-in formats.registry.RegisterIncoming<ExamplePacket>();registry.Freeze();
var bytes = PacketCodec.Encode(new ExamplePacket(0x1234));if (!bytes.AsSpan().SequenceEqual(new byte[] { 0xFE, 0x12, 0x34 }) || !registry.TryDecode(bytes, out var packet, out var opCode) || packet is not ExamplePacket { Value: 0x1234 } || opCode != 0xFE){ throw new InvalidOperationException("Packet round trip failed");}if (registry.TryDecode(new byte[] { 0xFE }, out _, out var failedOpCode) || failedOpCode != 0xFE){ throw new InvalidOperationException("Malformed-frame opcode was lost");}Console.WriteLine("Packet bytes and failure opcode verified");For variable packets, use BasePacket<T> with
[PacketHandler(opcode, PacketSizing.Variable, MinimumLength = ...)], validate the
whole declared frame and write its complete length header. See the built-in
ClientVersionPacket and ServerListPacket for examples of parsing and writing.
Register a game handler
Section titled “Register a game handler”Add a reference to Moongate.Server.Core. ExamplePacketHandler.cs can echo the
packet through the bounded sender without waiting for socket I/O:
using Moongate.Server.Core.Data.Sessions;using Moongate.Server.Core.Interfaces.Packets;using Moongate.Server.Core.Interfaces.Services;
public sealed class ExamplePacketHandler : IPacketHandler<ExamplePacket>{ private readonly IPacketSendService _sender;
public ExamplePacketHandler(IPacketSendService sender) { _sender = sender; }
public void Handle(GameSession session, ExamplePacket packet) { if (!_sender.TrySend(session.SessionId, new ExamplePacket(packet.Value))) { // The sender owns and observes connection cleanup. _ = _sender.DisconnectAsync(session.SessionId); } }}In Program.cs service composition, or a plugin’s Register callback, import
Moongate.Server.Core.Extensions and call:
container.RegisterPacketHandler<ExamplePacket, ExamplePacketHandler>();Handlers are singletons. Keep per-player state in the session/world, not mutable
handler fields. Handle executes on the game loop; keep it short and synchronous.
TrySend snapshots encoded bytes and returns admission status, not delivery
confirmation. Decide what to do when it returns false; the example disconnects.
For a handler that awaits database or network I/O, implement
IAsyncPacketHandler<TPacket> and register it with
RegisterAsyncPacketHandler<TPacket, THandler>(). Its HandleAsync runs off
the game loop. The handler receives a PacketContext rather than a mutable
GameSession. This example assumes an application-specific lookup service:
// Define this service in your plugin; keep its interface in a separate file.public interface IExampleLookup{ Task<ushort?> LoadAsync(ushort value, CancellationToken cancellationToken);}
public sealed class ExampleAsyncPacketHandler : IAsyncPacketHandler<ExamplePacket>{ private readonly IExampleLookup _lookup;
public ExampleAsyncPacketHandler(IExampleLookup lookup) { _lookup = lookup; }
public async ValueTask HandleAsync( PacketContext context, ExamplePacket packet, CancellationToken cancellationToken ) { var value = await _lookup.LoadAsync(packet.Value, cancellationToken);
if (value is not null) { context.TrySend(new ExamplePacket(value.Value)); } }}
container.RegisterAsyncPacketHandler<ExamplePacket, ExampleAsyncPacketHandler>();When the result must change game state, return to the loop with
await context.RunOnGameLoopAsync(session => { /* update session/world */ }, cancellationToken).
It returns false if the original session disconnected before the action ran.
An async handler must not mutate a session directly after an await.
Only one async packet may be in flight for a session. Packets the session sends
meanwhile wait, up to 1024 (PacketDispatchService.MaxPendingPerSession), and are
dispatched in arrival order when it finishes; one more is rejected and the client
disconnected. The executor accepts at most 64 operations at once and runs at most
four handlers concurrently.
Admission stays nonblocking. Disconnect and server shutdown cancel the
handler token; observe it in every awaited I/O call. Exceptions are logged
without packet payloads and do not stop the game loop.
Host integration
Section titled “Host integration”A new incoming packet needs two registrations. The handler registration does not add the opcode to the wire table. Add the packet type too:
container.RegisterIncomingPacket<ExamplePacket>();container.RegisterPacketHandler<ExamplePacket, ExamplePacketHandler>();At startup the host builds one registry from PacketTable plus every
RegisterIncomingPacket call, freezes it, and gives it to the framers, the login
and game listeners and the dispatcher. Outgoing packets need no registration to be
sent. A packet registered without a handler is decoded but rejected, and the game
server closes the connection, as it does for an unknown opcode. Register
IgnoredPacketHandler<T> for a packet the client sends that the server does not
act on yet.
The host also registers LoginSeed and an async AccountLogin handler. The latter
checks credentials against IAccountService, sends 0x82 for denied login or
an empty eligible realm list, and sends a filtered 0xA8 list after success.
The login-only host uses a dedicated ordered async connection pipeline; standalone
runs separate login and game listeners. Selection and handoff use Redis-backed
leases and one-use tickets (Implementation status). See
Transport and game ownership for connection lifecycle,
queue limits and overload policy, and Game loop and timers
for thread ownership and completion.
Made with ❤️ by Tom
