American Fugitive and the mystery of c05c


I’ve been recently playing American Fugitive and I’ve enjoyed it quite a bit. A nice callback to old GTAs with a serviceable story, fun mechanics but IMO lacking a bit in the combat system. Overall recommended.

So that’s my review. Trying to get the most out of the game was fun, so naturally as a completionist I wanted to explore what it takes to 100% the game.

Let’s look at the Steam achievements. There are of course a couple of them you will complete with the story, a nice metric for the developers of how many players were engaged by the game and the story to complete it.

But let’s skip dwelling on the fact that 77.2% of owners completed the first mission and only 8.1% completed the story. I wanna get all the achievements, so let’s look at the most diffcult ones.

Challenges and time trials

Complete all challenges and time trials with golden medals.

Honestly easy. Upon failing the first time trial horribly I began to feel scared, but the rest of them was easy to challenging, only like two of them were hard.

But I am about to lose my mind.

No Stone Left Unturned

Find every hidden stash.

The game contains 100 hidden stashes (maybe a callback to some collectibles in 3D GTAs?). Finding them all can be a terrible timesink. After completing the story, I found around 60 of them, not yet knowing if I wanted to find them all. Some of them are locked behind coded safes for which the code can be found out by reading hints scattered around the map.

The coded safes were fun, since the hints often contain a small puzzle. For example, one combination is “the year our grandmother died”, for which you will need to find out the name of the family and finally go to the graveyard to read her headstone.

The ones without hints… well… they sucked. A floating package in GTA Vice City, a floating horseshoe in GTA:SA or a bright note in Banjo Kazooie is easy to spot. Cracked wall in Zelda games is a bit more hidden but a piece of uneven ground blending into the surrounding soil or pigeons perching on a piece of railing (200 of them? Really GTA:IV?) are bullshot.

So hiding a stash behind the trees in a fixed camera perspective is a sure way to force players to just reach for an online map that some blessed soul spend days making and spend hours having it opened on a second monitor and trying to discern where the heck is the stash hidden.

Finding the rest of them took me about an hour and twenty minutes.

Stick Up Artist

Hold up 50 businesses.

Having looked at the stats screen and seeing that I stuck up only 13 times had me worried. But I found a place with three businesses nearby each other. Each stick up took me about 30 seconds with about 10 seconds of travel time, so on the whole about 30 minutes.

Gone In 60 Seconds

Steal 200 cars.

After completing pretty much all of the other achievements, the stat “Cars stolen” was at 86. After spending 20 minutes at a car rental agency and breaking into every car there, having to find more crowbars to use to break into more cars, I gave up at 115.

One Track Mind and Rollin’, Rollin’, Rollin’

Destroy 100 Enemy Tanks and Crush 250 Vehicles with a Tank.

These two achievements were going to break me. On a good rampage, I can get around 1 to 2 tanks per minute (if they decide to spawn - I once went for 5 minutes without a tank!) and crush 2 to 3 cars per minute. Using simple math and most optimistic predictions, I get:

max(50 minutes, 84 minutes) = 84 minutes

84 minutes when the spawns are good, my tank doesn’t get stuck on the crushed car (happens a lot BTW).

That’s where I decided to cheat.


Strategy of Cheating

When cheating, I basically have two options:

  • manipulating the memory of game
  • manipulating the save file

I decided to go with the save file, because I was tired of memory manipulation, where one value might be in multiple locations, referencing stale object not yet garbage collected or where one value is used for displaying and the other is the actual value and then looking for the accessors in ASM and finding the pointer references and…

Yeah, save files.

Find the save file

Since I am running the game through SteamPlay/Proton I will look in the wine prefix.

$ cd ~/.steam/steam/steamapps/compatdata/
$ ls
[...a lot of directories with just numbers as names...]

Oh, I need the Steam AppID. It’s the number in Steam’s url.

$ echo "https://store.steampowered.com/app/934780/American_Fugitive/" | grep -Eo "([0-9]+)"
934780
$ cd ~/.steam/steam/steamapps/compatdata/934780/pfx/drive_c/
$ find -name "*Fugitive*"
./users/steamuser/Temp/Fallen Tree Games Ltd/American Fugitive
./users/steamuser/AppData/LocalLow/Fallen Tree Games Ltd/American Fugitive

The Temp dir will be temporary files, lets ignore it.

$ cd ./users/steamuser/AppData/LocalLow/Fallen Tree Games Ltd/American Fugitive
$ tree .
.
├── output_log.txt
├── SaveGame
│   ├── Profile
│   │   ├── PlayerProfile.dat.bson
│   │   └── steam_autocloud.vdf
│   └── SceneMap_default
│       ├── AllManagers.dat.bson
│       └── steam_autocloud.vdf
└── Unity
    └── 628b636c-365e-4abf-b60a-31dffd89f81b
        └── Analytics
            ├── ArchivedEvents
            │   ├── 166891479600038.fb4f9fde
            │   │   ├── c
            │   │   ├── e
            │   │   ├── g
            │   │   ├── p
            │   │   └── s
            │   ├── 166891492000039.fb4f9fde
            │   │   ├── c
            │   │   ├── e
            │   │   ├── g
            │   │   └── s
            │   └── 166891492300040.fb4f9fde
            │       ├── c
            │       ├── e
            │       ├── g
            │       └── s
            ├── config
            └── values

10 directories, 20 files

Let’s ignore the Unity debug and analytics stuff and I’m left with:

SaveGame
├── Profile
│   ├── PlayerProfile.dat.bson
│   └── steam_autocloud.vdf
└── SceneMap_default
     ├── AllManagers.dat.bson
     └── steam_autocloud.vdf

SaveGame/Profile/PlayerProfile.dat.bson

$ vim SaveGame/Profile/PlayerProfile.dat.bson

reveals I’m probably dealing with a binary format with some some ASCII inbetween. Seeing such strings as KeyboardDefaultRemap and LockFPS, I see that these are probably global settings and move on.

SaveGame/SceneMap_default/AllManagers.dat.bson

$ vim SaveGame/SceneMap_default/AllManagers.dat.bson

Bingo. Inbetween binary junk I see

  • MGR_PLAYERPERSISTANTDATA.Cash
  • MGR_TIME_OF_DAY.totalGameTimeInMinutes

Lets open the file in something more suited for viewing binary.

$ xxd SaveGame/SceneMap_default/AllManagers.dat.bson
[...bunch of hex...]

Okay, thats a lot. First of all, I search for “[C|c]ars”

$ xxd SaveGame/SceneMap_default/AllManagers.dat.bson | grep -i cars
00007240: 7200 0000 0000 00c0 6540 0143 6172 7353  r.......e@.CarsS

CarsS what? Lets see more output:

$ xxd SaveGame/SceneMap_default/AllManagers.dat.bson | grep -A2 -i cars
00007240: 7200 0000 0000 00c0 6540 0143 6172 7353  r.......e@.CarsS
00007250: 746f 6c65 6e00 0000 0000 00c0 5c40 0148  tolen.......\@.H
00007260: 6967 6865 7374 5261 6d70 6167 6500 0000  ighestRampage...

CarsStolen, nice. Since I will be looking at the dump a lot, lets see some surrounding lines:

$ xxd SaveGame/SceneMap_default/AllManagers.dat.bson | grep -B3 -A3 -i cars
00007210: 4f63 6375 7069 6564 5665 6869 636c 6573  OccupiedVehicles
00007220: 546f 7765 6400 0000 0000 0000 0040 0154  Towed........@.T
00007230: 6f70 5370 6565 6446 6c61 6d69 6e67 4361  opSpeedFlamingCa
00007240: 7200 0000 0000 00c0 6540 0143 6172 7353  r.......e@.CarsS
00007250: 746f 6c65 6e00 0000 0000 00c0 5c40 0148  tolen.......\@.H
00007260: 6967 6865 7374 5261 6d70 6167 6500 0000  ighestRampage...
00007270: 0000 0000 3240 0143 6976 696c 6961 6e4b  ....2@.CivilianK

I am starting to see a pattern! Let’s rearrange the hexdump a bit:

OccupiedVehiclesTowed     00 0000 0000 0000 0040 01
TopSpeedFlamingCar        00 0000 0000 00c0 6540 01
CarsStolen                00 0000 0000 00c0 5c40 01
HighestRampage            00 0000 0000 0000 3240 01

40 01 repeats, so I assume (we might touch on that later) that it probably is just a separator. Aligning the hexdump, I get:

OccupiedVehiclesTowed     00 00 00 00 00 00 00 00
TopSpeedFlamingCar        00 00 00 00 00 00 c0 65
CarsStolen                00 00 00 00 00 00 c0 5c
HighestRampage            00 00 00 00 00 00 00 32

These values probably correspond to the ingame stats, seen here:

Stats screen

Assume:

  • OccupiedVehiclesTowed = “Occupied Vehicles Towed” = 2
  • TopSpeedFlamingCar = not present in stats screen
  • CarsStolen = “Cars stolen” = 115
  • HighestRampage = not present in stats screen

Next, converting the binary to human readable data.

The binary value is 8 bytes long, this leaves us with a couple of options of its data type (assuming all of them have the same data type)

LE (little endian)

  • 1x 8 bytes double
  • 1x 8 bytes int64 or long
  • 2x 4 bytes float
  • 2x 4 bytes int
  • … couple of others, like 8x 1 byte char, but those are unlikely

BE (big endian)

  • 1x 8 bytes double
  • 1x 8 bytes int64 or long
  • 2x 4 bytes float
  • 2x 4 bytes int
  • … couple of others, like 8x 1 byte char, but those are unlikely

Drumroll please…

                  Exp |   dblLE |   dblBE |               i64LE | i64BE |
OVT [00..] 00 00    2 |     0.0 |     0.0 |                   0 |     0 |
CS  [00..] c0 5c  115 | ~2e-319 | ~5e+138 | 6683341847017816064 | 49244 |

The rest is not included, since it’s similar nonsense.

This doesn’t seem right. What the hell is up with this binary number?

What does c05c mean???

.bson - seems like a familiar extension?

Some pieces of software use extensions to help with identifying the file type. Since file doens’t seem to recognize the save file as anything but binary

$ file SaveGame/SceneMap_default/AllManagers.dat.bson
SaveGame/SceneMap_default/AllManagers.dat.bson: data

I will assume that .bson will have some meaning. Seems familiar to .json

Googling bson gives me a Wikipedia definition:

BSON is a computer data interchange format. The name “BSON” is based on the term JSON and stands for “Binary JSON”. It is a binary form for representing simple or complex data structures including associative arrays, integer indexed arrays, and a suite of fundamental scalar types. BSON originated in 2009 at MongoDB.

Ah, OK, a database binary type stuff. Lets google for a parser or a spec

Oh, the standard looks pretty easy. But for my sanity’s sake, I will just use the premade parser.

bson parser

Let’s grab the package from npm and use it:

$ take bson-parser
$ npm init
$ npm install --save bson
$ cp package.json tmp && jq '.+{"type":"module"}' package.json > tmp && mv tmp package.json
import fs from 'node:fs/promises';
import {deserialize} from 'bson';

const SAVEFILE = process.env.HOME + '/.steam/steam/steamapps/compatdata/934780/pfx' +
  '/drive_c/users/steamuser/AppData/LocalLow/Fallen Tree Games Ltd' +
  '/American Fugitive/SaveGame/SceneMap_default/AllManagers.dat.bson';

(async () => {
  try {
    await fs.access(SAVEFILE, fs.constants.R_OK);
  } catch (e) {
    console.error(`Cannot access file ${SAVEFILE}`);
    return 1;
  }

  const saveData = await fs.readFile(SAVEFILE);
  const deserialized = deserialize(saveData);

  console.dir(deserialized);
  await fs.writeFile('./parsed-savefile.json', JSON.stringify(deserialized));
})().then(c => process.exit(c));
$ node ./parse.js
{
  'MGR_PLAYERPERSISTANTDATA.Cash': 39562,
  MGR_PLAYERPERSISTANTDATAUpgradePoints: 20,
  RespawnCount: 0,
  Unlocks: {
[... a lot more keys and values ...]
  }
}

Looks nice. Let’s poke around some keys to figure out what I can do and add some comments on what might the values be representing:

$ jq '. |= keys' parsed-savefile.json
[
  # Keys like `8aad93885f9dbdd44bfdaa8189b04272_10_data.SpecificUnlocks` skipped
  "AreaUnlocked",                            # Which part of town is unlocked
  "Clues",                                   # Which clues have been discovered
  "CurrentHealth",                           # Self-explanatory
  "DoingCharacterUnlockObjective",           # Self-explanatory
  "EquippedItem",                            # Self-explanatory
  "Explosive",                               # Explosive ammo count
  "Handgun",                                 # Handgun ammo count
  "HasPlayedIntroTankConversation",          # Self-explanatory
  "Inventory_Items",                         # List of items in inventory
  "LastIntroMessageType",                    # No clue
  "MGR_COLLECTABLE.CollectedItem",           # No clue, maybe flyers collected...?
  "MGR_COLLECTABLEViewedItem",               # No clue, maybe flyers collected...?
  "MGR_OBJECTIVESMGR.TutorialState",         # Self-explanatory
  "MGR_OBJECTIVESMGRObjectiveIndex",         # Self-explanatory, the progress in current mission
  "MGR_OFFENCE.LastATMOpenTime",             # No clue
  "MGR_PHONEBOOKMGR.AvailableCharacters",    # List of characters in phone book
  "MGR_PLAYER.CanRestoreCheckpoint",         # No clue, should be self-explanatory, isn't really
  "MGR_PLAYERPERSISTANTDATA.Cash",           # Amount of current cash
  "MGR_PLAYERPERSISTANTDATAUpgradePoints",   # Amount of current uprade points
  "MGR_STASHMGR.Stashes",                    # List of stashes UUIDs
  "MGR_STASHMGRPaintings",                   # List of paitings UUIDs
  "MGR_STATSMGR.Stats",                      # List of statistics
  "MGR_STATSMGRVehiclesDriven",              # List of vehicle UUIDs
  "MGR_TIME_OF_DAY.totalGameTimeInMinutes",  # Self-explanatory
  "MGR_TRAINERMGR.SeenTrainerTypes",         # No clue
  "MGR_VEHICLEJUMPMGR.Records",              # List of UUID-to-decimal of unqiue stunt jumps and their length
  "MetalDetectorEnabled",                    # Self-explanatory
  "Minigun",                                 # Amount of minigun ammo
  "ObjectivesCompletePerCharacter",          # A huge list of UUIDs, probably objective objects
  "RespawnCount",                            # How many times has the character respawned
  "RespawnOnObjectiveCount",                 # No clue
  "Rifle",                                   # Amount of rifle ammo
  "SMG",                                     # Amount of SMG ammo
  "SaidEscapeInDifferentLocation",           # No clue
  "SceneArea",                               # No clue, probably current area of character...?
  "Shotgun",                                 # Amount of shotgun ammo
  "Unlocks",                                 # List of upgrades and their levels
  "WornClothing"                             # UUID of worn clothes item
]

Although some of these values have unknown purpose, it’s nice seeing English being used (I worked on a French codebase once).

A whole lot of talk and no savefile editing! Let’s fix that!

Editing the save file

Since there is no checksum, the following should work:

import fs from 'node:fs/promises';
import {deserialize, serialize} from 'bson';

const SAVEFILE = process.env.HOME + '/.steam/steam/steamapps/compatdata/934780/pfx' +
  '/drive_c/users/steamuser/AppData/LocalLow/Fallen Tree Games Ltd' +
  '/American Fugitive/SaveGame/SceneMap_default/AllManagers.dat.bson';

(async () => {
  try {
    await fs.access(SAVEFILE, fs.constants.R_OK);
  } catch (e) {
    console.error(`Cannot access file ${SAVEFILE}`);
    return 1;
  }

  const saveData = await fs.readFile(SAVEFILE);
  const deserialized = deserialize(saveData);

  deserialized['MGR_PLAYERPERSISTANTDATA.Cash'] = 999999;

  const serialized = serialize(deserialized);
  await fs.writeFile('./savegame.gson', serialized);
})().then(c => process.exit(c));
$ node ./parse.js
$ cp "${PATH_TO_SAVE}" AllManagers.dat.bson.bak
$ cp savegame.gson "${PATH_TO_SAVE}"

I ran American Fugitive and… nothing! My cash kept resetting back!

This took me 30 minutes to debug.

Oh yeah, I forgot to disable Steam Cloud saves. D’oh!

Cheating Like a King

Lets change some more of the values:

import fs from 'node:fs/promises';
import {deserialize, serialize} from 'bson';

const SAVEFILE = process.env.HOME + '/.steam/steam/steamapps/compatdata/934780/pfx' +
  '/drive_c/users/steamuser/AppData/LocalLow/Fallen Tree Games Ltd' +
  '/American Fugitive/SaveGame/SceneMap_default/AllManagers.dat.bson';

(async () => {
  try {
    await fs.access(SAVEFILE, fs.constants.R_OK | fs.constants.W_OK);
  } catch (e) {
    console.error(`Cannot access file ${SAVEFILE}`);
    return 1;
  }

  const saveData = await fs.readFile(SAVEFILE);
  const deserialized = deserialize(saveData);

  console.dir(deserialized['MGR_STATSMGR.Stats']);

  deserialized['MGR_STATSMGR.Stats']['CarsStolen'] = 200;
  deserialized['MGR_STATSMGR.Stats']['AllFlyersFound'] = 15;
  deserialized['MGR_STATSMGR.Stats']['TanksDestroyed'] = 100;
  deserialized['MGR_STATSMGR.Stats']['VehiclesCrushedByTank'] = 250;

  deserialized['MGR_PLAYERPERSISTANTDATA.Cash'] = 999999;
  deserialized['MGR_PLAYERPERSISTANTDATAUpgradePoints'] = 99;
  deserialized['Rifle'] = 999;
  deserialized['Shotgun'] = 999;
  deserialized['Explosive'] = 999;
  deserialized['Minigun'] = 999;
  deserialized['Handgun'] = 999;
  deserialized['SMG'] = 999;
  deserialized['CurrentHealth'] = 999;

  const serialized = serialize(deserialized);
  await fs.writeFile(SAVEFILE, serialized);
})().then(c => process.exit(c));

Then running the game again

All achievements unlocked on the same day

Nice.

Making an editor

Why not? Let’s spin the framework wheel… Solid JS

$ npx degit solidjs/templates/ts-bootstrap american-fugitive-save-editor
$ pnpm install
$ pnpm run dev

Adding some bits of coding here and there and I have a complete editor!

Editor screenshot

You can see the complete code on GitHub.

And you can see it in action here.

Conclusion

I tried editing a single value in an unknown binary and I failed.

I used a library for parsing the binary and made a whole save editor. It was a fun practice round I guess…

See ya.

Edit this page on Github.com