Where I left off

Recap for anyone just landing. I’m rebuilding the server for Fury, a 2007 online game switched off in 2008. I have the program that ran on players’ PCs (the client). I do not have the program that ran in the data centre (the server). The plan is to read the client’s code closely enough to build a matching server from scratch.

Last post I got the decompiler, the tool that turns the game’s compiled code back into something a person can read, to handle a handful of real Fury functions. I spot-checked ten of them by hand. Eight came out clean. I called that a win and stopped for the night.

That number was hiding something.

What I tried

I did not want to pull functions out one at a time forever. I wanted the whole thing on disk: every class in every file, decompiled, in a folder I could grep. So I wrote a batch runner. Point it at a .u file, it walks every class inside, decompiles each one to its own .uc text file, and writes a list of anything that failed.

Fury’s code ships as fourteen of these .u files. I ran the batch over the small ones first. ServerFramework, GameFramework, Core, the animation file, the AI file. It just worked. Hundreds of classes, full source, almost no failures. That felt great for about ten minutes.

Then I ran it on GOGame.u, the big one, and paid attention to one class in particular: GORealmMasterLink.

That class is the client’s phone line to the master server, the machine that ran matchmaking, chat, guilds, who’s-online, and shoved you from the social hub into an actual match. If there is one class in this entire game I need to read perfectly, it is that one.

150 functions in it. 111 of them crashed the decompiler.

What broke

Every one of the 111 failed the exact same way:

1
2
3
4
UELib.DeserializationException: Couldn't load object
Function 'GORealmMasterLink.SocialAddFriend' ...
System.InvalidCastException: Unable to cast object of type
'UELib.Core.UnknownObject' to type 'UELib.Core.UField'.

“UnknownObject.” The tool read something, didn’t recognise what it was, filed it under “no idea,” and then a moment later tried to use it as a specific thing and threw its hands up.

Last time a crash like this meant the tool was off by a few bytes and reading junk. So I checked the byte positions against a function that did work. They lined up. Name here, parent there, first variable right where it should be. Not a misalignment this time.

So what was the “unknown” thing? I dumped the list of variables for one of the broken functions:

1
2
AvatarIDProperty  friendAvatarID
AvatarIDProperty  avatarID

AvatarIDProperty. And on other broken functions, GroupIDProperty, GuildIDProperty, TeamIDProperty.

Auran gave themselves custom variable types.

Here’s the idea. In most code, a player ID and a team ID are both just numbers, plain integers, and nothing stops you accidentally passing one where the other belongs. Auran didn’t want that. So they made AvatarID its own type, TeamID its own type, and so on. Under the hood each one is still just an integer. But the compiler now treats them as different things and slaps your hand if you mix them up. It’s a genuinely tidy bit of engineering and I’d bet it caught real bugs.

The decompiler, of course, has never heard of AvatarID. It knows int, it knows string, it knows the standard set. It hits AvatarIDProperty, goes “unknown,” and dies.

The fix is almost too small to write about. I told the tool: when you see any of these four Fury type names, treat it exactly like an int. Four lines.

Rebuilt. Re-ran the batch on GORealmMasterLink.

150 functions, 0 failures.

The batch over GORealmMasterLink before and after the fix. 111 of 150 functions throwing the same cast exception, then a clean run.
The batch over GORealmMasterLink before and after the fix. 111 of 150 functions throwing the same cast exception, then a clean run.

From 111 crashes to none, with four lines that say “this is basically an int.” While I was at it the batch turned up one more unknown type, double, sitting in the engine’s core file (a double is just a number with a decimal point that can be very precise). Same shape of fix, one more small class.

The other thing: everything was shouting numbers at me

With the classes decompiling, the contents still read badly. Every function call looked like this:

1
2
reservedName = __NFUN_201__(reservedName, "_", " ");
__NFUN_231__(__NFUN_168__("Login server address =", Address));

__NFUN_231__ is the tool saying “there’s a call to built-in function number 231 here and I don’t know its name.” 231 is Log. 168 is the $ you use to glue two bits of text together. 201 is a find-and-replace. The tool knew a call was happening, knew the arguments, just printed the number instead of the name.

Reason: Fury’s code is spread across fourteen files, and the names of all the built-in operations live in a couple of those files (mostly Core and Engine). When you decompile GOGame.u on its own, the tool never opens the other files, so it’s got the calls but not the dictionary.

So I taught it to peek. Before it decompiles anything, it now does a quick pass over all the sibling .u files sitting in the same folder, collects every built-in name it can find, and keeps that list handy. Same function as before, after the change:

1
2
reservedName = Repl(reservedName, "_", " ");
LogInternal("Login server address =" @ Address $ ", Account name =" @ userName);

That’s just readable now. Repl is replace, $ and @ glue text together, LogInternal writes a log line. Real code.

The payoff

I ran the batch over all fourteen files. About 2,000 classes decompiled to disk. The failures that remain are all the same cosmetic thing (more on that below), not missing logic.

And the first useful thing fell out almost immediately. This is from GOACGame, the mode the client runs during character creation and login:

1
2
3
4
5
Address        = ParseOption(Options, "login");
userName       = ParseOption(Options, "acc");
Password       = ParseOption(Options, "pass");
realmUpdateURL = ParseOption(Options, "realmurl");
reservedName   = ParseOption(Options, "rsvname");

When a game client connects to a server it sends a connect string, basically a URL with settings tacked on the end. This is the client pulling five specific values out of that string: the login server address, an account name, a password, a “realm update” web address, and a “reserved name.”

That is a chunk of the shape of “how a Fury client asks to join.” Named fields, straight out of the shipped code. When I get to standing up a fake server, this is one of the things it will have to understand. Now I know what to call them.

The five values the client pulls out of its connect string: login (server address), acc (account name), pass (password), realmurl (a “realm update” web address), rsvname (a reserved character name).
The five values the client pulls out of its connect string: login (server address), acc (account name), pass (password), realmurl (a “realm update” web address), rsvname (a reserved character name).

And once the whole thing is on disk you can just grep it, which is where the afternoon went. Two things jumped out.

One. Every time the server talks to its database, it does it the same way: call a stored procedure by name. A stored procedure is a canned operation that lives inside the database itself, so the game code never writes raw database queries, it just says “run the one called this.” Grepping for those calls gave me a list of 56 names, and the names are not shy about what they do: AVA_GetAvatarByName, AVA_AvatarTravel, EQU_GenerateLootItem, LDR_LogWarzoneAvatarStatistics, SVR_BanPlayerByAvatarName, GLD_GetGuildByName. That’s a partial menu of everything the old database was asked to do. I don’t have the database, but I have the list of questions it was expected to answer.

Two. I found the bouncer. When a match server decides whether to let you in, here is the actual check, cleaned up:

1
2
3
sessionKey = the "SessionKey" value from your connect string
avatarID   = look that key up in the pending-players list
if (avatarID <= 0) { reject you with "WRONGSID"; }

So before you ever connect, the master server tells the match server “expect a player with this session key.” That goes into a short list with a timer on it. You connect, you present the key, the match server finds you in the list and lets you in, or doesn’t and kicks you with WRONGSID. It’s a coat check. The key is your ticket.

A three-column sequence diagram. The master server tells the match server to expect avatar 4291 with key a3f9c1, which the match server holds in a timed pending list. The client then connects with that key in its URL. If the match server finds the key it lets the player in, otherwise it kicks them with WRONGSID.
A three-column sequence diagram. The master server tells the match server to expect avatar 4291 with key a3f9c1, which the match server holds in a timed pending list. The client then connects with that key in its URL. If the match server finds the key it lets the player in, otherwise it kicks them with WRONGSID.

(There’s a wrinkle: in this exact build that check looks like it’s switched off at the code level, and it might really live down in the C++ I can’t read yet. Wrote it down as an open question. Not going to pretend I’m sure.)

What I learned

“Eight out of ten” was a hand-picked sample and it lied to me. I chose those ten functions. The honest version of that test is: dump everything, then read the failure log. The failure log is where the truth is. The class I’d have cared about most was 74% broken and my ten-function sample sailed right past it.

And the pattern with these licensee games held for the fifth time. Every single change Auran made to the engine’s format has been small, local, and boring: one extra field here, four new type names there. Nothing sweeping. Nothing that needed a big rethink. Five surprises now, five one-liners.

Honest bit: it’s not spotless. Around seventy classes, almost all of them graphics and UI stuff (particle effects, sound nodes, menu widgets), come out with their defaultproperties block cut short. That’s the section that lists a class’s default settings. There’s a size mismatch in how Fury writes one kind of setting that I haven’t cracked yet. It does not touch a single line of actual logic, and none of those classes are server code, so I’m parking it with a note and moving on.

Next up

Start actually reading the server’s job. The login and “travel” path first: what the client sends to get in, what the master server was expected to send back, where a match server picks the player up. I have GORealmMasterLink in full now, and the whole codebase sitting in a folder. Time to read it.