Where I left off
Last post I listened to the client say HELLO to a server that wasn’t there. It
ended with two threads to pull:
- Read Fury’s own code for the rest of that handshake conversation, instead of assuming it copies Epic’s textbook version.
- Trace the shortest possible path through the server’s code from “a player connected” to “a character exists in the world and the client can watch it move.”
This post is both. No new experiment, this is a session of reading decompiled code with a map and a highlighter. It turned up one genuinely surprising thing and one thing that looks a lot like a bug until you understand what it’s telling you.
Thread 1: the rest of the handshake isn’t in the script
Quick one first. I went looking through all two thousand decompiled .uc files
for the words the handshake uses, HELLO, WELCOME, JOIN, CHALLENGE,
FAILURE, that family. I wanted to see Fury’s side of the conversation: what it
sends back, in what order, with what keys.
They’re not there. Not one of them, in any networking context. Every hit was a
false alarm, an unrelated list of combat failure reasons (FAILURE_TARGET_DEAD
and forty friends), a UI label, a duel-challenge message. Nothing that talks to
the network.
That sounds like a dead end. It isn’t, it’s just an answer I didn’t expect: the
entire opening handshake lives in the C++, not the script. Unreal Engine 3
handles that negotiation down in the engine’s compiled guts, in a class called
UControlChannel, and Fury never overrides any of it. The first time Fury’s
own code gets a say is well after the greeting is done, at the point the engine
turns to the game and asks “okay, do you want to let this person in?”
So there’s nothing in Fury’s script to check the handshake verbs against. The
spec for that stretch stays what it was: the old text-style Unreal handshake from
around 2004, which is well documented, plus the one hard fact I already have from
the capture (HELLO P=1 REVISION=0 MINVER=2327 VER=2797). The exact back-and-forth
after HELLO I’ll pin down the empirical way once I have a server, send a
plausible reply, watch what the client does next. I’m not going to guess it into
the code. (That’s a project rule, actually: no protocol guesses baked into code.
Unknowns get written down as unknowns.)
Fine. On to the interesting half.
Thread 2: tracing “connected” to “moving”
Here’s the thing I wanted. When a real Fury arena server was alive and a player joined a match, what actually had to happen, step by step, for that player’s character to appear and start taking movement input? If I can name every step, I can build the smallest possible version of it.
I had a good starting point from Phase 2. There’s a function called GOPostLogin.
The name says it all: the server has accepted this player, now set them up. I
expected it to be the place the character gets created. In a stock Unreal game it would be: accept
player → RestartPlayer → spawn a body → hand them the controls.
GOPostLogin doesn’t do that. It talks to the character as if it’s already
there:
| |
(“Pawn” is Unreal’s word for the physical character in the world, the thing that runs around. The “controller” is the invisible brain attached to it, whether that’s a human’s network connection or an AI.)
So if the body isn’t made here, where? I followed the calls backwards, and the answer is the surprising bit.
The character checks in before you do
In a Fury arena match, your character is spawned into the world before your game client ever sends a packet.
The sequence, in plain terms:
- The matchmaker, a separate server, off doing its own thing, decides you’re going into a particular match. It sends that match’s server a little dossier about you: your character’s ID, which team, which group, where to spawn, a session key. This happens over that other connection, the lobby one, the one I’m deliberately not touching yet.
- The arena server takes the dossier and, right then, builds your character. It picks a spawn point, spawns the combat pawn there, and parks a placeholder brain in it, a stand-in controller, basically an autopilot that does nothing. Your body is now standing in the arena, idle.
- It kicks off a load of your character’s actual data from the database (gear, abilities, stats): twelve separate database round-trips, filling in the body it just made.
- Then your client connects. The handshake happens. Your real controller,
meaning your network connection, gets created. And the server’s setup code
(
GOPostLogin, finally) finds the placeholder autopilot sitting in your body, evicts it, and drops you into the driver’s seat instead.
The mental model I landed on: it’s a hotel. The reservation comes in ahead of
you, the room gets made up, your bags are already in it. When you finally walk up
to the front desk, checking in isn’t “build me a room”: it’s “swap the
housekeeping key for mine.” That swap is a single Unreal call, Possess, and the
moment it happens the engine starts streaming that character’s position and state
down to your client. That’s the finish line I was tracing to: character exists,
client can see it move.
Why build it this way? Guessing: it means the match can be fully set up and even running, bodies in place, AI live, before all the humans have finished connecting, and a player who drops and reconnects just re-possesses the body that’s been standing there the whole time. It also means humans and bots are created through the exact same path, which is tidy.
For me it means the reservation step isn’t optional flavour. My minimal server has to fake that dossier and do the pre-spawn, or the check-in has nothing to check into.
The bit that looks like a bug
While tracing the check-in, I hit something that stopped me.
The check-in code identifies you by your character ID. It scans the list of reserved slots looking for the one whose ID matches yours, so it knows which body to hand you. And if it doesn’t find a match, it does this:
| |
It throws your connection away.
So I checked: on the path the shipped client build actually runs, where does your controller get told your character ID? And the answer is: nowhere. The only functions that stamp an ID onto a player’s controller are two login functions, and both of them are switched off in this build. (Remember from a few posts back: Fury ships as one binary with the server half of the code compiled in but disabled, behind a set of build-time switches.) The login path that is switched on is the plain stock one, and it never learned about character IDs because in a normal Unreal game there’s no such concept.
Put those together: if you took Fury’s shipped code exactly as it is, turned on
“be a server,” and let a client connect, the check-in code would look up
character ID 0, find nothing, and delete the connection. Every time.
That’s not actually a bug. It’s evidence. It means one of those build-time switches, the one that swaps the stock login for Fury’s own character-aware login, must be flipped to the other position for a real server build. I already knew there was a family of these switches and that the server was hiding behind them. This is the first one where I can say, from following the code alone, “this specific switch, this specific position, or nothing works.” It’s a small thing but it’s the first concrete entry on a list I’m going to need later.
So how small can the minimal server be?
Two routes out of this, and I’m not committing to either until I have a client to test against.
The faithful route. Do what the real server did: fake the matchmaker dossier, run the reservation, pre-spawn the body, stub out the database so the twelve lookups return canned data, let the client check in and possess. More code, but it’s the real path, and it’s the one that leads somewhere.
The shortcut route. There’s a branch in the check-in code that only runs for the full combat game mode. If my minimal server pretends to be a simpler kind of Fury game (not a combat arena, just a room), the code takes a different branch that spawns the body at check-in time, the stock way, with no reservation and no database at all. Less faithful, less code, and it might be enough to get a character standing in a level that I can send “walk forward” to. Which is the entire goal of this phase.
The shortcut probably isn’t where I end up. But “a character I can move” is a lower bar than “a working arena match,” and this phase is explicitly about clearing the lower bar first.
Next up
- Turn the messy trace above into a clean checklist of the C++ pieces I have to provide, it’s mostly written already, it just needs to be the tracked list.
- Pick the map: the smallest arena level that actually ships in my client, and confirm it loads.
- Then, finally, start typing a server: grab UDP 7777, unwrap the packet, and
answer that
HELLOwith something, which is also how I find out what the rest of the handshake actually says.