Where I left off
Last post the server got as far as CHALLENGE, and the client replied with two
messages: NETSPEED (how much bandwidth to send me) and LOGIN (challenge
response plus the name of the map it’s currently sitting on, entry.fmap, the
little loading-screen level). Then it stopped and waited.
It’s waiting for one specific word: WELCOME. In Unreal’s handshake that’s the
server saying “you’re in, your match is on this level, with this game mode, go
load it.” It’s the message that turns “connected to a socket” into “travelling
to a map.”
This post: the server says it, and the client does the thing.
First I had to stop dropping the mail
Before any of that, a bug from last time I didn’t notice because the handshake still limped forward.
A UDP packet in UE3 can carry more than one thing. It might be a message (a “bunch”), or it might be an acknowledgement (the client ticking off “got your packet #3”), or several of those, all stacked in one packet before the actual message.
My parser, when it hit an ack, just… stopped reading the rest of the packet. I inherited that behaviour from an earlier throwaway decoder that only ever had to look at one thing at a time, and it never bit me because the client had been sending its acks and its messages in separate packets.
This run it didn’t. Because the server now acks the client’s HELLO, the client
had an ack of its own to send back, and it stuffed that ack in front of its
NETSPEED/LOGIN message, same packet. My parser saw the ack, said “done here,”
and threw away the LOGIN sitting right behind it. So the server never saw a
login, never sent WELCOME, and I got a log full of nothing.
The fix is a small, very UE3 thing. Every packet ends with a single 1 bit and
then zero-padding out to the byte. So the real end of the data is “the last
1 bit in the packet, counting backwards.” Find that, and you can just keep
reading messages and acks until you reach it. Do that, and the packet unpacks
cleanly: an ack, then the message with NETSPEED and LOGIN inside it. There it is.
(I checked the ack format while I was in there. It’s just a flag bit and a 14-bit packet number, nothing after it. That’s a question I’d left open two posts ago with a “not modelled” note. Closed now.)
The word itself
I still didn’t know exactly how WELCOME should be written. Same problem as
every other verb in this handshake: it’s handled in Fury’s compiled C++, not in
any script I can read, and this is an early, licensee build of the engine so the
format write-ups online don’t quite line up.
So, same approach as always: build a candidate from the oldest Unreal references I can find, send it, and let the client’s reaction be the answer. The candidate:
| |
EL1_Mortem is the map I picked back in step 4, the smallest 1-v-1 arena.
GOGame.GOCombatGameEL1 is the matching game-mode class from the decompiled
script. Minimal: just the level and the mode, no display names, no extras. If
the client wanted more it’d ignore this and I’d see it sulk.
It didn’t sulk
Server sends WELCOME. Client’s log, immediately:
| |
That’s it changing levels. It read LEVEL=EL1_Mortem, took it as an
instruction, and started loading the actual arena. After eighteen years on the
transition screen, one sentence and it’s moving.
Then a burst I hadn’t seen before: eleven messages, all like this:
| |
The client is listing fingerprints of its own script files. This is the
integrity check, the mechanism a real server would use to go “your GOGame.u
doesn’t match mine, you’ve tampered with the client, get lost.” It’s the same
CalculateCRC I poked at all the way back in Phase 1, showing up on the wire
for the first time. My server doesn’t check them yet; I just wrote all eleven
values down, because they’re the retail build’s real checksums and I’ll want
them later.
And then, last message in the burst:
| |
“I’m ready. Put me in the match.”
Where it stops
Right there. The client sends JOIN and then goes quiet: just heartbeat
packets, five a second, for as long as I let it run. It’s sitting on the
EL1_Mortem loading screen with the bar not moving.
That’s expected, and it’s the whole next job. A UE3 client doesn’t leave the
loading screen when the map finishes loading; it leaves when the server hands
it a character to control. Nothing’s been handed over yet. The server’s side of
JOIN is: work through the login bookkeeping, find the player a slot, spawn a
pawn, and start replicating it, streaming that actor’s existence and position
down the wire. Until that happens the client just politely waits.
There’s also a chance the map itself can’t finish loading without the matchmaking services Phase 1 said this build leans on, but I can’t even find out until the server starts replicating, so that worry goes in the queue behind this one.
Next
The actor channel. On JOIN, drive the login path to a player slot (I mapped
that call graph two posts back), then open a channel and replicate the pieces a
client needs to exist in a level: the match state, the player info, a
controller, a pawn. That’s reusing the package-serialization code from the
decompiler fork for a completely different job than it was built for, which
should be interesting.
Somewhere on the other side of that, the loading bar fills and a character stands up in Mortem. That’s the one I’ve been walking toward since Phase 1.