eveofdarkness.org

What the client knows that the server doesn't

Last time the current 1.130 client got onto an OpenDAoC server, encrypted end to end. Getting on is the start. The next question is what goes wrong once you're there, and finding it by playing until something breaks is slow. So we counted instead.

Two sets of opcodes

A game client like DAoC's and a server like OpenDAoC or Dawn of Light (DOL) talk in small binary messages. A player action, a game event or a server-side trigger sends one, and the first byte of each message, the opcode, says what it is. 0xA9 is a position update, 0xAF a chat line or command, and so on. Messages run both ways, client-to-server and server-to-client.

For example, when your character enters the world, the server sends a burst of messages: your guild's message of the day, the objects and players around you, the time of day, the weather.

The client and the server each have, in their code, a table of the opcodes they understand. If we extract and compare the two tables, the differences are a to-do list for the OpenDAoC server.

The server's table comes from the OpenDAoC source. OpenDAoC grew out of DOL, and like DOL it registers a handler for each client opcode and has one method for each message it sends. A small script reads those out:

  • 80 client-to-server opcodes, every one with a handler.
  • 115 server-to-client opcodes, 112 of them actually sent to a 1.130 client.

The client's table comes from daoc-re, an EU-based project that studies the client's protocol. The client's network code has one place where every incoming message is dispatched by opcode to a handler, and a send function for each transport (TCP and UDP). daoc-re mapped those, which gives the client's side: about 130 opcodes it can receive, and the opcodes it sends.

The differences

  • 12 opcodes the client sends that OpenDAoC has no handler for. The server drops these without a word, so nobody notices. A few turned out to be simple: one is a "leaving the world" notice, and another tells the server why the game closed (a clean exit or a crash).
  • 22 opcodes the client can receive that OpenDAoC never sends. Each one is a feature the client is ready to show that the server doesn't drive. Some of these are clustered near the relic messages, and one is a "you can't log into this realm for N minutes" notice.
  • 8 opcodes the other way round: the server knows them, the client doesn't. These are leftovers for older clients, safe to ignore.

How much is new in 1.130?

Very little. The authentication and encryption changes were the part of 1.130 that broke freeshards, so the obvious next question was whether more changes in the newer client would bite us. daoc-re ran the same mapping on the 1.127 game.dll, the client freeshards have used for years, and we compared the two. 1.130 adds one new message, and no existing message changed its layout. Nearly every missing opcode above was already missing with 1.127.

Does DOL know more?

OpenDAoC forked from DOL, so the obvious place to look for names, or handler logic, for the missing opcodes is DOL's own opcode tables. They are almost identical to OpenDAoC's: 115 server messages on both sides, and 78 client messages to OpenDAoC's 79. OpenDAoC added one, "client filter settings", which the official client sends three times on entering the world. And one message moved: DOL sends the ground-target change as 0xDF, OpenDAoC as 0xEC. Of the 34 missing opcodes, DOL has a name for exactly one, and it doesn't send that one either. The missing opcodes date from before the fork, so naming them means analyzing the client and live sessions.

How an unknown opcode gets a guess

An unknown opcode in the client has no name, only code that builds or reads it. In daoc-re's mapping, four kinds of clue narrow it down:

  • Who calls it. The code that sends a message usually sits inside the feature that uses it. If the builder is called from the code behind a slash command, or from code that prints a [Task] message, that's a strong hint.
  • What's next to it. Handlers for one feature tend to be written together and end up next to each other in the binary. An unknown handler sitting among the keep or relic handlers is probably about keeps or relics.
  • Its number. Opcodes come in families. 0x61 to 0x6E are nearly all keep messages (plus the war map's realm bonuses), and the relic-map pair is 0x45/0x46, with two of our missing opcodes, 0x44 and 0x47, on either side. A gap in the middle of a family is a good bet for that family. Features added later tend to get opcodes and code of their own, apart from the older ones, which is one reason the 1.127/1.130 comparison is useful.
  • Its shape. A one-byte message with nothing in it is a notice or a poke; one carrying a 24-character string probably carries a name.

None of these is proof. Here is where some of them led:

Opcode Clue Guess What the sessions showed
C→S 0xAC, 0xB8 sent from the client's start-up and shutdown code leaving / quitting notices Confirmed. 0xB8 carries a reason: clean exit or crash
C→S 0x1D, 0x42, 0x43 called from the slash-command and area-list code something to do with entering a zone Partly. Always sent together on entering the world; the official server sends no reply of its own
C→S 0x52 built near the character-select code; carries a name character delete Wrong. Delete works another way (below); 0x52 is still unknown
C→S 0xB3 0xB4 0xE6 0xE7 builders sit right next to the "looking for group" builder the group finder window Not so far. The group finder's Find uses the already-known LFG message
C→S 0x98 built inside the /appeal command code an appeal sub-request Open. The appeal dialog didn't send it
C→S 0x41 its caller prints [Task] messages the task system Open
S→C 0x44 0x47 0x59 0x5A 0x5B handlers next to the relic-map handlers OpenDAoC does send minotaur relics Open. The official server sends 0x59 on world entry (below)
S→C 0xA6 the one opcode new in 1.130 something added late Partly. Sent right after "You have entered …"

Checking guesses against a live session

An opcode table says that a message exists, not what it's for. For that we watch real sessions: our own client on our own server, and a recording of the official client on the official server, doing the same things on both.

That stopped us from filing a bug. One unhandled opcode sat right next to the character screen's code, so we assumed (along with other clues) that "character delete is missing." On the server, delete worked fine: since 1.104 it rides inside the character-create message, which OpenDAoC already handles. The unhandled opcode is something else, still unidentified.

It also gave us better leads. On entering the world, the official server sends five kinds of message that OpenDAoC never does. One is the message new in 1.130, sent right after "You have entered …". Another arrives 29 times in a row, one per numbered item, each tagged with what looks like a realm. The game works without them, which makes them the best candidates for "the UI shows less on OpenDAoC than on the official servers."

Some things matched exactly. The group finder's Find button and the war map ask the same questions and get the same kinds of answers on both servers.

Bugs along the way

Testing the leftovers turned up a handful of small OpenDAoC bugs: a status command whose reply the server fails to fill in, a "stuck" appeal from the client's own dialog that the server rejects, and a database connection pool that goes stale after a few idle days and stops players from entering the world. We'll report them to the OpenDAoC team along with the pull request for 1.130 support.

What's next

Two threads. First, work out what the five missing world-entry messages mean, and whether OpenDAoC should send them. Second, the game's UDP channel: when it comes up, the client can misread a packet badly enough to crash. That's why the client stayed up for about 13 minutes and then died "at random." Our recorders now capture UDP, so we can compare both servers' UDP traffic.

As before, this is written up as observed behaviour on the wire. The opcode tables and protocol findings will be public; client internals are not published here.

Updated 28 October 2026 to credit daoc-re for the client-side opcode tables.