Skip to content

4 · The C# server

The problem: Clash of Clans is an online game. It's still running today — but the live servers only serve the current version and won't talk to a 2012 client, and there's no original server source code to fall back on. A v1.70 client has nothing to connect to.

So a running client needs a backend that speaks its exact, decade-old protocol. We rebuilt that backend from scratch as a C# server. The interesting part isn't the infrastructure — it's how we reproduced the game's deterministic, lockstep relationship between client and server, and how we got the game logic into the server without re-typing thousands of methods by hand. To understand the approach, it helps to know how Supercell themselves built the original.

How the original was built

Clash of Clans has two sides that must agree perfectly: the client simulates a battle so it can animate it, and the server simulates the same battle so it can be the authority on what really happened. If the two simulations ever diverged — a slightly different damage number, a different random roll — the game would desync.

The way Supercell kept them in lock-step was to write the game logic once, in C++, and share it. The client, itself a C++ app, used that code directly. The server, however, ran on the JVM in Java — a managed, memory-safe environment much better suited to running thousands of untrusted player sessions without crashing. To bridge the two, Supercell maintained an internal C++→Java converter that mechanically translated the shared C++ logic into Java, so the server ran a faithful copy of the very same simulation the client did.

flowchart TB
    LOGIC["Shared game logic<br/><b>written once, in C++</b>"]
    LOGIC -->|compiled directly| CLIENT["Client<br/>(C++ app on iOS)"]
    LOGIC -->|internal C++ → Java converter| SERVER["Server<br/>(Java on the JVM)"]

That single design decision — author the logic in C++, but run the server on a safe managed runtime, and transpile between them — is the blueprint we followed. We just pointed it at a different language.

Our version: C# instead of Java

We chose C# for the server for exactly the reasons Supercell chose Java. The server is the authoritative side of an online game, exposed to untrusted clients, and it simply has to stay up. A managed runtime buys the properties that matter there, and that the original C++ couldn't offer:

  • A bad packet can't crash the process. In C++, a stray pointer or bad cast is undefined behavior — a segfault that takes the whole server down with it. In C#, a malformed message throws an exception that gets caught, logged, and turned into a single dropped connection. Everyone else keeps playing.
  • No memory bugs to hunt. Garbage collection and bounds checking remove an entire class of server-killing failures: use-after-free, buffer overruns, leaks.
  • Safe casts. A wrong type is a caught exception, not silent corruption.

And just like Supercell, we don't re-type the game logic — we transpile it. Our converter is called cpp2cs, and it does to C# what Supercell's tool did to Java: it takes the reverse-engineered C++ simulation recovered back in chapter 1 and mechanically produces idiomatic, memory-safe C#.

flowchart LR
    CPP["Reverse-engineered C++ logic<br/><i>(recovered in chapter 1)</i>"]
    CPP --> T["cpp2cs<br/>(our C++ → C# converter)"]
    T --> CS["Generated C#<br/><i>deterministic · memory-safe</i>"]

The one hard requirement is determinism: the C# output has to compute bit-identical results to the original C++ — the same damage, the same timers, the same random rolls — or the server and client would disagree about the game. So cpp2cs preserves C++'s exact semantics (field layouts, virtual dispatch, the difference between a pointer and a reference) rather than producing merely "equivalent-looking" C#. The result is the best of both worlds: the logic is the genuine 2012 game, but it now runs on a runtime that won't fall over.

One reverse-engineering effort, reused twice

Chapter 1's work pays off on both sides of the network. The client assembles the recovered code and lifts it to run the game; the server takes the same recovered C++ and transpiles it to C#. Two independent reimplementations of one original — which then have to agree on the wire.

Staying in sync: deterministic lockstep

The hardest thing to reproduce isn't any single message — it's the model the whole protocol is built on. Clash of Clans keeps client and server in agreement not by streaming every change over the network, but by having both sides run the identical simulation and exchanging only the inputs to it. For that to work, the simulation has to be perfectly deterministic: from the same starting state and the same inputs, every device must reach the exact same result, down to the last bit. Two design choices make that possible:

  • No floating point. Floats round differently across compilers and CPUs, so the game uses only fixed-point integer math — same inputs, same outputs, everywhere.
  • The command pattern. The player never changes the game state directly. Every action — placing a building, training a troop, launching an attack — is packaged as a command, and commands are the only thing that can alter state. That means an entire game can be replayed from its starting state plus an ordered list of commands.

The simulation advances in fixed steps called ticks, and both the home village and a battle are simulated this way on both sides. A session stays synchronized like this:

sequenceDiagram
    participant C as Client
    participant S as Server

    S-->>C: initial state: your village at tick 0
    Note over C: simulate locally, tick by tick<br/>player acts, record commands
    C->>S: EndClientTurnMessage<br/>client tick T, state checksum, commands
    Note over S: queue commands,<br/>simulate forward to tick T,<br/>apply each at its tick
    S->>S: compute its own checksum
    alt checksums match
        Note over C,S: in sync: state advances and is persisted
    else checksums differ
        S-->>C: OutOfSyncMessage
        Note over S: close the session
    end

Step by step: on login the server sends the authoritative starting state — the village as it last knew it. The client simulates forward from there, animating what the player sees and recording each action as a command stamped with the tick it happened on. Periodically the client sends an EndClientTurnMessage that says, in effect, "here's where I am": it carries the current client time in ticks, a checksum of the entire game state at that tick, and the commands the player ran during the turn. The server queues those commands and fast-forwards its own simulation to the client's tick, applying each command at the tick it belongs to, then compares checksums. Match means the two simulations are provably identical and play continues; a mismatch means the client has drifted, and because the whole model depends on the two never diverging, the server doesn't try to paper over it — it sends an OutOfSyncMessage and closes the session.

This is exactly why cpp2cs has to be bit-for-bit deterministic, and why Supercell shared one C++ simulation between client and server in the first place. The game uses no floating point at all precisely for this reason — but the discipline goes further: a single uninitialized byte, a different integer width, or an evaluation that depends on platform quirks would shift a checksum and desync the game.

On the wire

Underneath that model, messages are framed simply. The client connects over TCP, and every message is a small fixed header followed by an encrypted body:

┌────────────┬─────────────┬──────────────┬────────────────────────────┐
│ Msg type   │   length    │   version    │  payload                   │
│  2 bytes   │   3 bytes   │   2 bytes    │  (RC4-encrypted)           │
└────────────┴─────────────┴──────────────┴────────────────────────────┘
 └───────────── 7-byte header ────────────┘

The message type is a 16-bit ID whose range encodes direction and meaning — login, the turn message above, chat, and so on. The body is scrambled with the RC4 stream cipher using the fixed key baked into the 1.70 client; there's no certificate or key exchange, because that's how the 2012 protocol worked, and we reproduce it faithfully. Each message serializes itself, and because those message classes come through cpp2cs from the same source as the client's, both sides encode them identically.

Inside the server

The server isn't a monolith. It's a set of small, independent services that each own one concern and talk to each other over a NATS message bus through a custom fast binary driver — no service calls another directly, which keeps them independently deployable and restartable.

flowchart TB
    C(["Game client"])
    C <-->|"PiranhaMessages · RC4 over TCP / WebSocket"| PX
    PX["Proxy — gateway<br/>sockets · RC4 · sessions<br/>(thin, stateless)"]
    PX <-->|publish / subscribe| NATS

    NATS{{"NATS bus<br/>custom fast binary driver"}}

    NATS <--> HO["HomeOwner<br/>live village simulation<br/>(stateful, per player)"]
    NATS <--> AL["Alliance<br/>membership · chat · donations"]
    NATS <--> MM["Matchmaking<br/>attack targets"]
    NATS <--> HR["HomeRepository<br/>avatar streams · battle reports"]
    NATS <--> GS["GlobalChat · Scoring"]

    HO <--> DB[("PostgreSQL")]
    AL <--> DB
    HR <--> DB

At the edge is the Proxy — a deliberately thin, stateless gateway. It accepts each client's TCP connection (or WebSocket, for the web build), runs the RC4 layer, and authenticates the player into a session. From there it does no game logic of its own: it decodes each incoming PiranhaMessage and republishes it onto NATS for whichever service owns that message type, then forwards anything coming back down to the client. Because it holds no state, you can run as many Proxy instances behind a load balancer as you need.

The live game runs in HomeOwner — the stateful heart of the server. Each player's village is loaded into memory here, advanced by the deterministic simulation, and persisted back. This is also where the lockstep from above lands, and the detail worth stressing is that a command is not a special side-channel — it's an ordinary game message. The client's EndClientTurnMessage carries the tick, the checksum, and the list of commands; HomeOwner unpacks them, feeds them into the simulation, steps the village forward tick by tick, then compares the checksum and saves the new state. Attacks are simulated here too, so a tampered client can't simply claim a win it didn't earn.

Around HomeOwner sit the other services, each owning its slice of the game and reachable over the same bus: Alliance (membership, troop donations, alliance chat), Matchmaking (choosing an opponent's village to attack), HomeRepository (the per-player streams — battle reports and invitations — and serving a village's data when its owner is offline), plus GlobalChat and Scoring (ELO and the leaderboards that rank players). When one service needs another — HomeOwner asking Matchmaking for a target, say — it sends a request over NATS and gets a reply, still without a direct dependency.

Durable state lives in PostgreSQL. A village is stored as compressed JSON guarded by a compare-and-swap token (so two updates can't clobber each other), alongside the accounts, avatars, alliances, and stream entries; the stateful services reach it through a pooled Npgsql driver. And the asynchronous side — being attacked, receiving a donation, a new alliance message — rides NATS too: a session subscribes to the subjects it cares about (its alliance, its own avatar stream), a service publishes to that subject when it appends an entry, and the Proxy pushes the update down to every subscribed client the moment it happens.

Underneath it all, the deterministic math, RNG, and compression shared with the client keep every service computing the same numbers the client does.

What this layer produces

A safe, modern backend the original client can't tell apart from the real thing — same protocol, same encryption, same game math — but one that shrugs off bad input and never leaks memory. Wire the client to it and the loop is closed: a 2012 game, fully revived. The last chapter shows it running everywhere.