How 2004 RuneScape fit a multiplayer RPG into 56k dial-up · jkm.dev
How 2004 RuneScape fit a multiplayer RPG into 56k dial-up
How 2004 RuneScape fit a multiplayer RPG into 56k dial-up<br>28 May 2026<br>Updated: 29 May 2026Added correction regarding Ctrl key. In early versions this was “force run”, in a later<br>update it was changed to “invert movement mode”.
development<br>runescape<br>gaming<br>networking
In 2004 I played too much RuneScape on a 56k modem that died the moment Mum picked up the phone. A 3D world, up to a couple of thousand players on a server, dozens on screen at once - in the browser, on 5 kilobytes per second. It worked. Let’s follow a single step and see how.<br>As a child I was too preoccupied with picking flax and killing goblins to think about how this worked. The answer, however, is a sustained, almost obsessive exercise in not wasting bytes. So, let’s click one tile north of where we’re standing, and trace every byte that crosses the wire from that click, to the server, to the screen of another player.<br>Central fountain, Varrock Square<br>Methodology#<br>The detail in this post comes from a decompiled 2004 RuneScape 2 client. Snippets are rough translations from that decompile, tidied up in places for readability but with the logic intact.<br>The core principles aren’t identical across versions, but most of them run all the way from RuneScape Classic (2001) to present-day RuneScape 3 and, of course, Old School RuneScape.<br>If you played RuneScape in the early 2000s and are still in possession of a hard drive from that era, please check out the RuneScape Archive Project. They do great work to catalogue historic RuneScape versions which are otherwise lost to time, and every contribution is valuable.<br>Constraints#<br>Let’s look at some of the constraints that Jagex were working with at the time.<br>Bandwidth. A 56k modem syncs at 56 kilobits per second downstream, and less upstream, minus any protocol overheads and line noise. Call it 5 KB/s down and a lot less up. Broadband was available in British homes by 2000, but it wasn’t until the late 2000s that the majority of UK households had a broadband connection, so plenty of players were on dial-up.<br>Java applet, in a browser, in 2004. Java applets ran in a security sandbox, which meant no raw native sockets and no UDP. Every byte travelled over a single TCP connection, in-order and with per-segment overhead.<br>A 600ms server cycle. The RuneScape game server advances in discrete cycles (or ticks) of roughly 600 milliseconds. Every cycle, for every player, the server has to work out everything that player can now see and ship it before the next one.<br>The cipher layer, briefly#<br>After the login handshake completes, before any game packets are sent, a small encryption layer is set up. This one’s not about saving bytes; it’s the only encryption in the stack (outside of some RSA encryption in the login handshake), and it’s here because the opcode it protects is the very thing every later section depends on.<br>Every packet begins with an “opcode” byte: a small integer saying what kind of packet this is. That opcode (and only that opcode) is enciphered with a stream cipher called ISAAC. There are two streams in play - one for traffic from client to server, and one for the reverse direction. Both sides need both streams: the client enciphers what it’s about to send and deciphers what just arrived, and the server does the same in mirror image (per connected player).<br>Both streams are seeded from a shared four-integer key. The client generates two of those integers itself; the other two come from the server as part of the handshake. The server-to-client stream then uses the same seed with 50 added to each word - enough to keep the two directions from sharing a keystream:<br>this.outboundCipher = new ISAAC(seed);
for (int index = 0; index Enciphering on the way out is one line:<br>public void putOpcode(int opcode) {<br>this.putByte(opcode + this.outboundCipher.value());<br>And on the way in, the mirror image:<br>this.currentOpcode = (this.currentOpcode - this.inboundCipher.value()) & 0xFF;<br>So the packet body isn’t encrypted, only the opcode. As we’ll see later, the opcode is what tells you how to read the rest of the packet, and where one packet ends and the next begins. Without it, the body is just a wall of bytes, so enciphering that one byte was the cheapest possible defence against third-party packet parsers.<br>Sending a walk request#<br>We’re going to look at what happens when you click on a tile one square north, and how that gets transmitted to the server.<br>Before any networking occurs, the client runs a breadth-first search using the local collision map to build a path from where you are to where you clicked (an easy search, in this case), and then writes the packet for the server to read. The pathfinding is standard so I won’t go into it here.<br>The first part of the packet is the...