The protocol

What Worlds actually says over the wire, explained rather than tabulated. If you want every byte of every command, that is what docs/ in the server repository is for — this page is the part you read first so the tables make sense.

The shape of it

It is a binary protocol over plain TCP, historically on port 6650. There is no encryption, no compression and no framing beyond a length byte. Everything multi-byte is big-endian. The version in use is 24.

len 1 byte ObjID 1 or 5 cmd 1 byte payload 0 … n the whole frame must stay under 256 bytes under protocol 24. Reach it and the client drops the connection. A real one — a buddy-list update saying "fuwn is online": 09 01 1e 04 66 75 77 6e 01 │ │ │ │ └─────────┘ └─ online = 1 │ │ │ └─ UTF length = 4 │ │ └─ command 30 (0x1e) │ └─ ObjID 1 └─ 9 bytes follow
The shape of every packet. Length, who it is about, what it is, then the arguments. Thirty-one commands all fit this.

Length includes itself

The first byte counts itself. A minimal packet — short id, no payload — is three bytes and starts with 0x03. Getting this off by one is the first bug everyone writes.

A packet cannot reach 256 bytes. Not "should not" — the client throws. If you need to send more, split it. Sixty avatars appearing at once is several packets, not one.

There is a long-packet mode for versions above 24, and it is broken: the write path and the read path do not agree, so a long packet cannot round-trip. It never runs because the client never goes above 24. Advertise 24 and stay under 256.

ObjID: who the packet is about

Every packet names an object. There are two encodings, and the first byte tells you which:

first byte != 0   →  [id]                    a short id, one byte
first byte == 0   →  [0x00][UTF: name]       a long id, a name

That works because a long id always begins with byte zero and a short id never is zero, so there is no ambiguity. Short ids from 2 to 252 are handed out by the server; the client only ever sends 1, 253, 254 and 255 itself.

Strings are not Java strings

Worlds' "UTF" is not Java's writeUTF. The length prefix is one byte, not two, so a string is capped at 255 bytes. The encoding itself is modified UTF-8, which means NUL encodes as two bytes (0xC0 0x80), not one.

Implement this with a standard UTF-8 library and you will desynchronise on the first null byte and spend an evening finding out why.

There are two property formats

This is one of the easiest traps in the whole protocol. Properties are how almost everything is described — your avatar, your privileges, the server's own settings — and there are two incompatible encodings for them. Each command uses one specific format, and mixing them breaks parsing.

FormatShapeUsed by
Old [id][UTF value], repeated until the packet runs out PROP, SESSINIT, SESSEXIT, APPINIT
New [id][flags][access][value], where the value is binary or a string depending on a flag PROPSET, PROPUPD, FINGREP
One bad byte loses the whole list. If any property in a list contains invalid UTF, the client discards the entire list and skips the rest of the packet, printing "Property list discarded; invalid UTF property." So a single malformed value does not cost you that value — it costs you everything in the packet.

Getting in: the handshake

client server PROPREQ what are you? PROPUPD the property list — the announcement rides here SESSINIT name, password, client build SESSINIT reply your ObjID, privileges, update interval ROOMIDRQ which room is "GroundZero#Reception"? ROOMID this number. The client caches it forever
Logging in. Six messages and you are standing in a room. The server's property list comes before anything else — which is why a capability announcement can live there without any client noticing.

Six messages between opening a socket and standing in a room. Two things about that sequence are worth noticing.

The server describes itself before you log in. The property list comes back before the client has sent a name or a password. That is what makes a capability announcement possible: there is a place in the conversation where the server can say what it is, and the client already stores everything it receives there while only reading the ids it recognises. An unknown property is kept and never looked at.

The room id is cached forever. The client asks "what number is this room called?" once, and remembers the answer permanently — there is no command that invalidates it.

Room ids must be deterministic. If your server hands them out from a counter and then restarts, returning clients will subscribe to an id that now means a different room, or nothing. Derive the number from a stable hash of the room's long name, or persist a table. There is no way to tell the client to forget.

The thirty-one commands

Grouped by what they are for, rather than by number.

GroupCommandsWhat they do
Session SESSINIT (6), SESSEXIT (7), APPINIT (8) Logging in and out. APPINIT only exists for protocol versions below 18 and is not needed.
Properties PROP (3), PROPREQ (10), PROPSET (15), PROPUPD (16) Describing anything. Your avatar, your privileges, the server's settings, an object's state.
Presence APPRACTR (12), DISAPPR (11), REGOBJID (13) Somebody arrived, somebody left, here is the short id you should use for them from now on.
Movement LONGLOC (1), SHORTLOC (4) Absolute position, and a compressed delta. SHORTLOC is the one that makes a crowded room affordable.
Talking TEXT (14), WHISPER (17) Public chat and private chat. Both go through the server.
Rooms ROOMCHNG (5), ROOMIDRQ (20), ROOMID (21), SUBSCRIB (22), UNSUBSCR (23), SUB-DIST (24), TELEPORT (18), CHANNEL (31) Where you are, what you can see, and how far. SUB-DIST changes your interest radius — how much of the room is relayed to you.
People FINGREQ (27), FINGREP (28), BUDDYLISTUPDATE (29), BUDDYLISTNOTIFY (30) Profiles and the friends list. The names are exactly as old as they sound.
Redirection REDIRECT (25), REDIRID (26) Send this client to another server, optionally into a named room.

Numbers 2, 9 and 19 are unassigned or unimplemented in the client.

Rules that will bite you

If you write your own implementation, these are the ones that cost a debugging session each.

RuleWhat happens otherwise
Echo public chat back to the sender The client does not display your own public chat. If the server does not send it back, the person typing sees nothing and assumes they are disconnected.
Room ids must be deterministic Returning clients end up in the wrong room, or nowhere, and there is no way to clear their cache.
Never advertise server type 3 The client aborts the connection.
Stay under 256 bytes per packet The client throws. Check it in the serialiser, not at each call site.
A clean 404 beats an invented file A missing file is handled; a malformed one is not. Serving a fabricated languages.lst makes the client exit during startup.
Use the right property format per command Parsing desynchronises and the client discards the whole list.

What the protocol cannot do

Worth being honest about, because two of these come up constantly:

Reading it yourself

Everything above was read out of the client. If you want to check any of it, decompile your own copy and look at NET/worlds/network/ — 84 classes, and WorldServer.java is the state machine at the centre of it. The inside the client page says where to start.

The byte-level reference — every command, every constant, the handshake with real dumps — is in docs/ in the server repository.