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:
| |
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:
| |
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:
| |
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:
| |
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:
- Grab UDP port 7777.
- Unwrap the packet: the first 14 bits are a packet counter, then a short header, then the text.
- 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.