Where I left off

Quick recap for anyone landing here first. I’m rebuilding the server for Fury, a 2007 online game that has been offline since 2008. I have the client, the program that ran on players’ PCs. I do not have the server. The plan is to read the client’s code closely enough to rebuild the server from scratch.

Last few posts were about poking the client with a stick to see what still runs. That is done. The verdict was “everything runs, and there is no shortcut,” so now I am into the slow part: taking the game’s code, which ships in a compiled form a human cannot read, and turning it back into something a human can.

That is called decompiling, and there is an open-source tool for it.

What I tried

A tiny bit of background on the shape of the problem.

Unreal Engine games from this era ship their gameplay code as .u files. Think of a .u file like a .zip full of code: there is a header describing what is inside, then a big table of contents, then the actual contents. Fury has fourteen of these files. The big one, GOGame.u, is about 9.5 MB and its table of contents lists nearly 50,000 entries.

The layout of a .u file: header, name table, export table, then the code. Auran tucked an extra 4-byte field in two places, right after the GUID and on the end of every export row.
The layout of a .u file: header, name table, export table, then the code. Auran tucked an extra 4-byte field in two places, right after the GUID and on the end of every export row.

The tool I am using is called UELib (with a viewer on top called UE Explorer). It is well made and it supports dozens of games. It does not support Fury. Nobody has ever pointed it at Fury, because until now nobody had a reason to.

So the job for this session was: get UELib to open a Fury file without falling over.

First run, straight at the smallest file:

1
2
3
4
Package Version: 407/36
Build: Unknown
Generations Count: 1359147804
Unhandled exception: Unable to read beyond the end of the stream

Version 407/36 is the tool correctly reading Fury’s “I am this exact flavour of Unreal Engine” stamp. Build: Unknown means it has no specific handler for that flavour, so it is guessing. And then it reads a number that should be about 2 and gets 1.3 billion, tries to read 1.3 billion things, and hits the end of the file.

That number, 1359147804, is not random garbage. It is four bytes that Auran added to the file format that stock Unreal Engine does not have, being read as if they were the next real field. Everything after that point is off by four bytes. An earlier code audit had actually predicted this exact thing, so I knew roughly what I was looking at.

Two rows of labelled boxes. Stock Unreal reads a “generations count” field right after the package GUID and gets 2. Fury slips an extra 4-byte field in front of it, so the unpatched tool reads that instead and gets 1.3 billion.
Two rows of labelled boxes. Stock Unreal reads a “generations count” field right after the package GUID and gets 2. Fury slips an extra 4-byte field in front of it, so the unpatched tool reads that instead and gets 1.3 billion.

Registering Fury as a known build. UELib has a big list of games with their version stamps. I added Fury to it (407/36), and added an instruction: when the file is Fury, right after the part where the format normally has a unique ID, read and throw away four extra bytes.

Rebuild. Run again.

1
2
3
4
5
Build: Fury
Fury post-GUID field: 0x78605B61
Generations Count: 2
EngineVersion: 2797
CookerVersion: 0

The header parses now. And 0x78605B61 is interesting on its own, because the earlier audit had independently written down that exact value for this file. Two different methods agreeing is a good sign that I am reading the format correctly and not just making the crash move somewhere else.

The crash did move somewhere else though.

What broke (or what I didn’t expect)

New crash, a little further in, while reading the table of contents:

1
2
3
Unhandled exception: Index was out of range
   at UELib.UnrealReader.ReadName()
   at UELib.UExportTableItem.Deserialize

So the header was fixed but the table-of-contents entries were still misaligned. Each entry describes one thing in the file: its name, what type it is, how big it is, where it lives. The tool was reading a name reference and getting a number that pointed off the end of the name list.

The extra four bytes after the unique ID were not the only thing Auran added.

I dropped out of the decompiler and wrote about forty lines of Python to read the table of contents by hand, byte by byte, trying different guesses about the layout. The one that worked: stock Unreal layout, plus four extra bytes on the end of every single entry. With that assumption, every entry in the file lines up perfectly, and the table ends exactly where the header said it would, to the byte. Same thing on a second file. That is not a coincidence, that is the format.

Best guess is that both sets of extra bytes are checksums. Auran seems to have sprinkled little “is this chunk intact” numbers through the file format. Fine. The decompiler does not care what they mean, it just needs to know they are there and step over them. Two more lines added.

Rebuild. Run again. And this time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
enum TravelFailCode
{
    TFC_Succeed,              // 0
    TFC_NotConnected,         // 1
    TFC_InTrade,              // 2
    TFC_InvalidID,            // 3
    TFC_WrongMapType,         // 4
    TFC_InstanceNotFound,     // 5
    TFC_AlreadyTravelling,    // 6
    TFC_InCombatTransfer,     // 7
    ...
    TFC_TeleportFullInstance, // 16
    TFC_Max                   // 17
};

That is real Fury source code, reconstructed from the compiled file. An enum is just a named list of possibilities, and this one is the complete list of reasons the game can give for why moving a player from the social hub into a match might fail. “You are in a trade.” “That instance is full.” “You are already travelling.” Eighteen of them.

The rebuilt enum next to a plain-English read of what its codes mean.
The rebuilt enum next to a plain-English read of what its codes mean.

Then there is a bigger one, PerformanceType, which is 104 entries long and reads like a tour of the entire back end. Login queue size. Logins per second. How many players are in matchmaking for each game mode. Billing purchase counts for the last minute, five minutes, and hour. Three different skill-rating algorithms (Glicko, TrueSkill, and something Auran called Whippy) tracked separately per mode. This is a list of every number the old server hardware reported about itself, and I now have all of them by name.

I sat there reading server telemetry counter names off a screen for a game that has not had a running server in sixteen years. That was a good moment.

Enums are the easy case though. The real prize is functions, the actual logic, and that took the rest of the day.

First attempt at a class with real logic gave me the class name, what it inherits from, and then a row of empty function (); lines. No names, no bodies. That looked bad.

It turned out to be mostly my own fault. The little command-line tool I was driving was not actually asking the library to read the contents of each function, just the outline. Once I fixed that, real code started coming out:

1
2
3
4
function NotifyPlayerConnected()
{
    local int PlayerIndex;
}

That is a genuine Fury function, logic and all, rebuilt from the compiled file. The thing I was most worried about, whether the compiled logic comes back out in readable form, just happened in front of me. For simple functions it works.

Then the tool hit a wall on the complicated ones. Any function that did something real, walked an array, read a field off another object, called a helper, came out half-finished with an error in the middle.

Compiled code is a stream of tiny instructions packed end to end with no padding. To read instruction five you have to have read instructions one through four at exactly the right length, because that is the only thing telling you where five starts. Get one instruction’s length wrong by a single byte and everything after it is nonsense.

So I picked one broken function, found its raw bytes in the file, and walked through them by hand with a pen, instruction by instruction, next to what the tool thought it was reading. About thirty instructions in, they disagreed. The instruction for “read a field out of a struct” is one byte longer in Fury than in the stock engine. Auran added a flag to it. The stock tool reads the short version, ends up one byte behind, and never recovers.

One byte. I added a line that says “for Fury, this instruction has the extra byte,” rebuilt, and ran it again. This is the actual output, warts and all:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
function NotifyPlayerLogin()
{
    super.NotifyPlayerLogin(Pawn);
    if (__NFUN_119__(GOAvatar(Pawn), none))
    {
        if (__NFUN_130__(__NFUN_129__(bAvatarBodyDataComplete), bDoesPrecacheAvatarBodyData))
        {
            AddAvatarBodyData(GOAvatar(Pawn));
            if (AreAllAvatarsLoaded())
            {
                bAvatarBodyDataComplete = true;
                GetAvatarSkillRanks();
            }
            ...

The same function run through the old build and the fixed one. Before, it dies partway down with a byte-alignment exception. After, the whole body comes out.
The same function run through the old build and the fixed one. Before, it dies partway down with a byte-alignment exception. After, the whole body comes out.

That is a real function off Fury’s match server, the code that runs when a player joins a fight, reconstructed from a 16-year-old compiled file. It loads your character’s body data, waits for everyone, then pulls skill ratings. I have not seen this code before. Nobody outside Auran has.

The __NFUN_119__ bits are built-in operators and functions, !=, !, and so on, that I have not taught the tool the names of yet. It knows there is a call there and what its arguments are, it just prints a number instead of !=. That is a lookup table I will fill in, not a real problem. The structure, every branch and loop and call, is correct.

I spot-checked ten functions from the match-server classes. Eight came out whole. The other two hit some different small difference I have not chased yet.

What I learned

The thing that actually worked was the least clever option available: print the raw bytes, get a pen, and walk them one at a time until the tool and I disagreed. I had been putting that off for something smarter. There was nothing smarter. Half an hour with a pen found a one-byte difference that a day of reading code around it had not.

And the licensee pattern held again. Every single thing Auran changed about this engine’s file format has been small, local, and the same shape: one extra field, tucked into an otherwise standard structure. Four of them now, and every one announced itself as a crash or a number that was off by a few bytes.

Next up

Chase the last two broken functions the same way. Build the name lookup so calls read as < and GetAvatarSkillRanks instead of __NFUN_150__. Then the mechanical bit I have been putting off: point the fixed tool at all fourteen files and dump every class to disk, so the rest of the project has the whole codebase to read instead of me pulling one function at a time.