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.
0x61to0x6Eare nearly all keep messages (plus the war map's realm bonuses), and the relic-map pair is0x45/0x46, with two of our missing opcodes,0x44and0x47, 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.