A few weeks ago I bought four copies of the 2003 game Airline Tycoon Deluxe with the intent of playing some multiplayer games with family members. Steam didn’t list it as a multiplayer game but, from memory, I was pretty sure that it supported playing over a local network. Unfortunately, when I tried this it worked okay with two players but with three or four players the game froze on the dreaded end-of-day screen:
In this blog post we come up with a patch for version 1.03 of the game’s executable,
At.exe, to fix this particular problem. There is an excellent open source debugger, x86dbg, we use for making sense of the x86 assembly code and making changes. It also has the ability to save and apply patchfiles that you can use to patch your
At.exe if you want to.
For this task I enlisted the help of my enthusiastic 13 year old son.
Surprisingly, there is some source code available for a version of the game too. This makes for a very useful reference to make sense of the x86 assembly code.After some digging in the source code a particular bit of code in
Sim.cpp caught our interest. The bug happens just before the game hits midnight, so this if statement seemed very relevant:
To try and get some insight as to this code’s involvement in the bug we need to find the associated addresses in the executable. A really helpful function in x86dbg is that you can look for instances of specific integer constants. By looking for
15F900 (which is in hexadecimal) we found this if statement in the executable at address
As it turns out, the if statement condition is triggered at the time of the bug. You can confirm this by placing a breakpoint at
004CAC90 and watching it get hit. However, if you place a breakpoint at
004CAC9A it never gets triggered. That’s it then! When we hit midnight,
Sim.NewDay() never returns!
The best part about this discovery is that we can now bisect the code. Also, by following the call addresses we have a way of mapping assembly code into c++ code. Through many
hours days of work we found that
SIM::NewDay() which calls calls
PLAYER::NewDay() four times which then in turn calls a function called
NetGenericSync a few times with different values for a parameter called
SyncId. It is in
NetGenericSync where the game gets ‘stuck’ in an infinite loop that it is unable to escape.
Let’s have a closer look at
NetGenericSync . This is what the c++ source code of this function looks like:
PumpNetwork is a function that processes incoming messages. When the application receives a message from another player it sets
GenericSyncIds[c] to the
SyncId in the received message, where
c is the player’s index.
By doing extensive logging we found that the following is happening (simplified):
- Player 1 sends a message to all players (with
- Player 2 sends a message to all players (with
- Player 1 sees player 2 is sync’ed (on
SyncId=A) but player 3 is not.
- Player 2 sees player 1 is sync’ed (on
SyncId=A) but player 3 is not.
- Player 3 sends a message to all players (with
- Player 2 sees player 1 and 3 are sync’ed on
SyncId=Aexits the function and moves on to
- Player 2 sends a message to all players (with
- Player 1 sees player 3 is sync’ed (on
SyncId=A) but player 2 is not because it already moved on to
Basically, before player 1 could see player 3 had sync’ed on
SyncId=A, player 2 had already moved on to
SyncId=B, making it impossible for any player to move forward from that point onward.
For your perusal, the
.exe’s assembly code for
NetGenericSync looks like this:
The two big jumps at the start are for the two
if statements. Then, the outer yellow arrow is the return jump for the
while loop. The
for loop’s return jump is the inner yellow arrow. The
break statement is the little jump at
0046D618. The comparison,
c==4, can be found at
0046D71C and the call to
0046D724. Hopefully that gives you enough of a reference to work out what this assembly code does.
Fixing the logic
To fix the logic we have to stop checking the latest, most up-to-date
SyncId for each player and instead keep track of which players have sync’ed to our
SyncId at some point (even if they have moved on to a new
Fortunately, the way that
NetworkPump() is implemented means we do see every single change in in
GenericSyncIds provided we check each player following a return from
NetworkPump. This means we should we should check every player’s sync condition following every return from
NetworkPump and remember when a player meets the sync condition indefinitely. We then break out the while loop once all players have met this condition at some point.
In short, we need to change our logic to something like this:
We introduced a 32-bit
sync variable which is initially
0. Whenever player
c meets the sync condition we remember this by setting the associated byte of
0x01. We check whether all players have at some point matched the sync condition by checking
sync is equal to
Fixing the exe
Making the logical change above by changing the .
exe file is… difficult. You can’t just add a line of code to an executable, as it would change all the addresses in the code that follow, which messes jumps and function calls. You also can’t just add a local variable or call another function without being very careful and changing a bunch of stack-handling code. The sensible option is to jump to some unused bit of
.text and insert additional instructions there.
And this is exactly what we did. Our changes follows the C++ changes above closely, except it has a bunch of jumps and instead of using a local variable we use one of the last addresses in the
.exe’s data section,
0x006D0FF0, to store the
The patchfile looks like this (you can download it here):
>at.exe 0006D69A:68->E9 0006D69B:01->4B 0006D69C:17->08 0006D69D:AA->22 0006D69E:AD->00 0006D6D9:42->45 0006D719:02->B1 0006D71A:EB->E9 0006D71B:AF->F4 0006D71C:83->07 0006D71D:7D->22 0006D71E:FC->00 0006D71F:04->E9 0006D720:75->DA 0006D721:02->07 0006D722:EB->22 0006D723:07->00 0028DEEA:00->68 0028DEEB:00->01 0028DEEC:00->17 0028DEED:00->AA 0028DEEE:00->AD 0028DEEF:00->C7 0028DEF0:00->05 0028DEF1:00->F0 0028DEF2:00->0F 0028DEF3:00->6D 0028DEF9:00->E9 0028DEFA:00->A1 0028DEFB:00->F7 0028DEFC:00->DD 0028DEFD:00->FF 0028DEFE:00->81 0028DEFF:00->3D 0028DF00:00->F0 0028DF01:00->0F 0028DF02:00->6D 0028DF04:00->01 0028DF05:00->01 0028DF06:00->01 0028DF07:00->01 0028DF08:00->0F 0028DF09:00->84 0028DF0A:00->1D 0028DF0B:00->F8 0028DF0C:00->DD 0028DF0D:00->FF 0028DF0E:00->E9 0028DF0F:00->11 0028DF10:00->F8 0028DF11:00->DD 0028DF12:00->FF 0028DF13:00->8B 0028DF14:00->45 0028DF15:00->FC 0028DF16:00->C6 0028DF17:00->80 0028DF18:00->F0 0028DF19:00->0F 0028DF1A:00->6D 0028DF1C:00->01 0028DF1D:00->E9 0028DF1E:00->A9 0028DF1F:00->F7 0028DF20:00->DD 0028DF21:00->FF
I won’t go into details as to how this implements the functional changes.
To share our fix with the world we are making the patch file available. With this patch file you can create a patched executable as follows:
- Locate the Airline Tycoon Deluxe executable
- Make a backup of your
At.exefile so that you can revert to the original file if need be.
- Download and install x86dbg.
- Open your
At.exefile in x86dbg.
- In x86dbg, go to File > Patch File.
- Select Import to import our patch file.
- Select Patch File to write out a patched executable.
- Run the patched executable.
Note that all players will need to have the patched executable for the fix to work.
In our experiments the above patch fixes the multiplayer bug. We suspect there may be a few more unresolved issues in the game that may arise in multiplayer mode, though. Either way, let us know how you get on!
The patchfile has only been tested for the
At.exefor version 1.03 of the game (MD5
8a2b226a2453bdf46dd0709007b2a2f9). If you have a different executable this patch will likely not work. ↩