Der Entwickler James Monger (jkm.dev) hat einen RuneScape-2-Client aus dem Jahr 2004 dekompiliert, um zu zeigen, wie Jagex ein Multiplayer-Rollenspiel über ein 56k-Modem mit nur rund 5 KB/s Downloadrate laufen ließ. Der Artikel verfolgt einen einzigen Klick, "eine Kachel nach Norden laufen", Byte für Byte vom Mausklick über den Server bis zum Bildschirm eines anderen Spielers.

walk packet encoding
this.outboundStream.putOpcode(ClientToServerOpcodes.WALK_TILE);
this.outboundStream.putByte(4 + 2 * (pathLength - 1) + 1);
this.outboundStream.putShort(this.playerPositionX + firstX);
this.outboundStream.putShort(this.playerPositionZ + firstZ);
for (int i = 1; i < pathLength; i++) {
 this.outboundStream.putByte(this.pathX[i] - firstX);
 this.outboundStream.putByte(this.pathZ[i] - firstZ);
}

Drei Einschränkungen prägten das Design: die winzige Bandbreite des Modems, die Tatsache, dass das Spiel als Java-Applet in einer Sandbox lief, die nur eine einzige TCP-Verbindung ohne rohe Sockets oder UDP erlaubte, sowie ein Server, der in festen Zyklen von rund 600 Millisekunden lief und dabei jede Runde neu berechnete, was jeder Spieler sehen konnte.

Bevor Spieldaten fließen, richten Client und Server einen ISAAC-Stromchiffre ein, der nur das eine Opcode-Byte am Anfang jedes Pakets verschlüsselt, nicht den restlichen Inhalt. Zwei Streams laufen parallel, einer pro Richtung, beide aus vier beim Login ausgetauschten Ganzzahlen abgeleitet; der Server-zu-Client-Stream addiert 50 auf jeden Seed-Wert, damit sich beide Richtungen keinen Schlüsselstrom teilen.

Ein einzelnes "Lauf zu Kachel"-Paket besteht aus Opcode, Längenbyte, den absoluten Startkoordinaten als zwei 16-Bit-Werte und danach nur den Eckpunkten des Pfads als vorzeichenbehaftete Ein-Byte-Deltas statt jeder einzelnen durchquerten Kachel. Ein letztes Byte hält fest, ob Strg gedrückt war, was in frühen Versionen den Laufmodus erzwang und später die Bewegungsumschaltung umkehrte. Feldreihenfolge und Byte-Offsets wurden zwischen den Spielversionen zudem als Anti-Cheat-Maßnahme durchgemischt.

Der Server arbeitet jeden 600-ms-Zyklus in fester Reihenfolge ab: eingehende Pakete lesen, Spiellogik ausführen, ausgehende Updates zusammenstellen, dann senden. Ein Paket, das kurz vor Zyklusbeginn eintrifft, wird fast sofort verarbeitet, eines kurz danach wartet bis zu 600 Millisekunden.

Der Server antwortet jedem Client pro Zyklus mit einem einzigen zusammengesetzten "Player-Update"-Paket, das den lokalen Spieler, bereits bekannte Spieler in der Nähe und Neuankömmlinge abdeckt. Der Großteil davon wird bitweise statt byteweise gepackt: Ein unveränderter Spieler kostet ein einziges Bit, ein Spieler, der gerade gelaufen ist, etwa sieben Bit, und die Position eines neu sichtbaren Spielers wird als zwei vorzeichenbehaftete 5-Bit-Deltas relativ zum Betrachter gespeichert statt als volle 16-Bit-Weltkoordinaten. Nur detaillierte Änderungen wie eine neue Animation oder gewechselte Ausrüstung wechseln zu byteweise ausgerichteten Feldern und fügen 44 bis 80 Byte pro Spieler hinzu, wenn Aussehensdaten übertragen werden.

Mongers Rechnung: Das ausgehende Lauf-Paket kostet rund sieben Byte, das Update, das ein anderer Spieler in der Nähe über den Schritt erhält, etwa neun Byte, alles innerhalb eines 600-ms-Fensters mit reichlich Reserve bei 5 KB/s für Dutzende gleichzeitiger Spieler.

Die größere Pointe des Artikels: Client und Server verhalten sich wie ein einziges, über eine Leitung aufgeteiltes Programm. Beide Seiten einigen sich im Voraus auf Standardwerte, Sichtweiten und Paketformate, sodass auf der Leitung fast nichts explizit gesagt werden muss. Moderne Web-APIs tauschen diese Effizienz gegen lose Kopplung und unabhängige Deployments, was der richtige Kompromiss ist, wenn Bandbreite billig ist und Änderungsgeschwindigkeit wichtiger ist als Bytes.