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.
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.
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
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.
| Format | Shape | Used 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 |
Getting in: the handshake
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.
The thirty-one commands
Grouped by what they are for, rather than by number.
| Group | Commands | What 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.
| Rule | What 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:
- Passwords are sent in clear text. There is no key exchange to hide them behind, so no client change can fix it. See Trust & safety.
- There is no way to invalidate a cached room id. No command exists for it.
- Inventory and shared object state exist in the client but are not implemented here yet. The table for inventory is in the client's own data file; the shared-state mechanism is the client's "sharer".
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.