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:

1
WELCOME LEVEL=EL1_Mortem GAME=GOGame.GOCombatGameEL1

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:

1
UGOGameEngine::OnMapChangeStart> map='EL1_Mortem'

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:

1
2
3
4
CRC File=..\GOGame\ScriptFinalRelease\Core.u Checksum=-1268279994
CRC File=..\GOGame\ScriptFinalRelease\Engine.u Checksum=-930819108
CRC File=..\GOGame\ScriptFinalRelease\GOGame.u Checksum=1682201980
...

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:

1
JOIN

“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.