Where I left off

Last post ended with a to-do: point an unmodified Fury client at a network port with nothing listening on it, and watch what it tries to say. Whatever comes out first is the opening line my rebuilt server will have to learn to answer.

That’s this post. It’s a small experiment and it went really well, so I want to walk through it properly.

The setup

Two pieces.

A fake ear. A tiny script that grabs a network port on my own machine and just… listens. It writes down every byte anything sends to it and never, ever replies. Forty lines of Python. In networking terms it’s a brick wall with a notepad.

One detail I didn’t want to guess at: which kind of port. Network traffic comes in two main flavours, TCP (a phone call: connect, talk, hang up, nothing gets lost) and UDP (postcards: fire and forget, might not arrive). Unreal Engine’s networking has a class literally named TcpNetDriver, which sounds like it settles the question. It does not. Despite the name, Unreal games have always run gameplay over UDP. So rather than trust the name, I had the script listen on both on the same port number and waited to see which one lit up.

The client, aimed at nothing. Normally a Fury client only tries to join a match after it’s logged in, picked a character, and been handed an address by the matchmaker. I don’t have any of that. But you can also just tell it an address on the command line:

1
Fury.exe 127.0.0.1:7777

127.0.0.1 is “this same machine”. 7777 is Unreal’s traditional gameplay port. And here’s the first nice surprise. Back in Phase 1, every time I tried to make the client host a game it detonated instantly: a crash dump and the repair tool. I half expected the same here. Instead the log said:

1
AnalysisLog: Server Address: 127.0.0.1

It read the address, accepted it as somewhere to connect to, loaded a blank placeholder level, and started dialling. The join path is wide open from the command line. Only the host path is nailed shut.

What it said

The UDP side of my fake ear lit up. The TCP side stayed dark the whole time. So: gameplay is UDP, name notwithstanding, question settled.

Then the actual content. I was braced for a wall of binary gibberish, because that’s what Unreal’s gameplay traffic normally looks like: bit-packed, no padding, unreadable without a decoder. The first packet was 53 bytes. After I peeled off an 11-byte wrapper, the rest was this, in plain ASCII:

1
HELLO P=1 REVISION=0 MINVER=2327 VER=2797

That’s it. That’s the client’s first word. A greeting, a platform number (P=1 = Windows), and a set of version numbers so the two sides can check they’re compatible before going further.

It’s readable. It’s basically a chat protocol. HELLO, some KEY=VALUE pairs, done. I did not expect that and it makes my life much easier.

[SIDE NOTE] This is actually an old way of doing it. The original Unreal games, back around 2000, ran their whole join negotiation as little text lines like this. Later Unreal Engine 3 games mostly switched to a compact binary version of the same conversation. Fury, in early 2008, is still using the text style, at least for the opening. For me that’s pure upside: I can read the negotiation with my eyes instead of writing a parser to see what’s going on.

The rhythm

The client didn’t say HELLO once. It said it, waited about a second, said it again, and kept that up. In between, every fifth of a second, it sent a 2-byte blip: just a packet counter and nothing else, the network equivalent of “still here, still waiting.”

This is the engine’s reliability system doing its job. It sent an important message, got no acknowledgement, and it’s going to keep resending until it does.

After about ten seconds of silence it gave up, and it gave up politely:

1
FAILURE Connection timed out, no connection to server

It sent that line, wrote error link-dead to its log, and closed. No crash dump. That’s worth saying out loud, because in Phase 1 everything ended in a crash dump. This is the first time I’ve watched this client fail at something and just… shrug and exit. A timeout is a failure it was built to expect.

Ran it twice

I captured the whole thing a second time, cold. The two recordings are byte-for-byte identical, except for the packet counter that’s supposed to change.

That matters more than it sounds. It means the greeting contains no timestamp, no random number, no session token, nothing unique to that one run. My server won’t have to recognise or echo back some per-connection secret at this stage. The opening is fixed. I can hard-code the expected greeting, hard-code a reply, and move on to the interesting part.

For the record, the numbers my server will have to accept: platform 1, revision 0, min-version 2327, version 2797. A client reporting anything else is presumably where a “your game is out of date” rejection would fire, but I can’t see that path until I have a server that can send one.

What I’ve actually got now

The rebuilt server’s first job just went from vague to concrete:

  1. Grab UDP port 7777.
  2. Unwrap the packet: the first 14 bits are a packet counter, then a short header, then the text.
  3. Read the line. If it starts with HELLO, reply on the same channel with the next line of the script.

I don’t yet know what “the next line of the script” is. From Epic’s own engine code the rough shape is: client says hello, server sends back a welcome (or a challenge first), client sends its login details, server says “welcome, load this map.” But I’m not going to assume Fury follows Epic’s script exactly. The next step is to read Fury’s own decompiled code for the other half of this conversation and confirm the real sequence.

Next up

Two threads. One: trace the shortest possible path through the server’s own code from “a player connected” to “a character exists in the world and the client can see it move.” That’s the smallest thing worth calling a success. Two: pin down the rest of the handshake verbs from Fury’s script rather than Epic’s, so I’m building to Fury’s actual conversation and not a plausible-looking guess.

Then I start typing a server.