Skip to content

About

Trying to build a savegame editor for the game Mad Max with the goal of resetting convoys and other parts of the game.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Latest commit

 

History

26 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Mad Max (2015) save file format & editor

Reverse-engineering notes and a working save editor for Avalanche Studios' Mad Max (2015, Steam app id 234140).

Status: solved, including convoys. Saves can be decoded, edited, re-sealed and loaded by the game. Destroyed static world objects can be put back. Scrap can be set to any value. And cleared convoys can be fully restored - verified in game, on a live playthrough and on a 100% save: the routes come back marked in red on the map, and driving to one gives a complete, working convoy.

convoy.py reset SAVE.sav OUT.sav --slot N --state 2

--state 0 brings them back undiscovered instead - present in the world, not yet on the map.

That took a while. Six earlier attempts failed because they edited records that describe a convoy rather than the one that governs it; the history is in docs/FINDINGS.md and the answer is in docs/GAME-FILES.md.

The save's largest table is fully decoded too. The 1520-entry roster is a verbatim copy of the game's own economy table, matching 1520 of 1520 rows on ID and profile - so every entry now has a resource type, a capacity, a threat class and a world position read from the game's data rather than inferred. That is also what settles the counts: 37 camps, 97 scarecrows, 35 snipers, 13 convoys, 30 minefields. See docs/ECONOMY.md.

There is a browser editor at https://rick-thompson.github.io/MadMax_Savegame_Editor_Project/ - it runs the tools in this repo in your own tab, with no upload and no third-party requests. See web/README.md, including how adding a capability to it is one entry in tools/webapi.py.

This is published so other people can take it further. Everything here is reproducible from the sample saves in data/.


What works

capability tool verified
decode / re-encode any save madmax_save.py yes
pass the game's integrity check after an edit madmax_save.py reseal yes, loads in game
read the header (slot, playtime, timestamps, format gen) madmax_save.py info yes
list / diff the record stream madmax_save.py records / rdiff yes
map and diff the section-2 tables sec2.py yes
diff the hash-named property store tail.py yes
edit a property record in place tailedit.py yes
map a payload and show the gaps mapsave.py yes
recover names for the hashes names.py see HASHES.md
extract files from the game archives arcx.py yes
decode ADF files and graph scripts adf.py yes
read SARC bundles (.bl) sarc.py yes
read RTPC entity data (.blo) rtpc.py yes
disassemble XVM gameplay scripts xvm.py yes
restore a destroyed scarecrow, sniper tower, etc. mmworld.py --restore-type yes, in game
destroy objects in bulk (for testing) mmworld.py --destroy-type yes, in game
set scrap resource.py scrap IN OUT N yes, in game
inspect the 13 convoys convoy.py list yes
restore cleared convoys convoy.py reset --state 2 yes, in game
name every roster entry from the game's data economy.py tables / dump yes
list collected / missing relics and hood ornaments relics.py list / missing yes
refill scrap, water, fuel and ammo pickups economy.py refill --type Scrap not yet in game

What is still open

The property store's key scheme. Its keys are 64-bit and opaque: nothing in 101,426 real game names, nor in 398,623 strings harvested from the binaries, matches under lookup3 or five other functions. You can edit a record once you know what it governs, but you cannot look one up by name. See docs/HASHES.md.

This is narrower than it used to be. "Every key in a save is a hash" was too strong: the 1520-entry roster is keyed by EconomyResource.ID, a world object id the game ships in economyresources.economyresourcesc, and those are now resolvable by lookup rather than by hashing. Only the property store is still opaque.

The integrity value at offset 0 is not solved in closed form, so every edit must preserve file length. The delta-carry workaround makes that a non-issue in practice, and every tool here enforces it.

Roughly 30% of the payload still does not parse - mapsave.py shows the gaps. Nothing needed for the edits above lives there, but it is where the next findings will come from.


Quick start

Requires Python 3, no dependencies.

# where the game actually loads from (Linux/Proton)
SAVES=~/.steam/debian-installation/userdata/<SteamID3>/234140/remote

python3 tools/madmax_save.py info    "$SAVES/GameSave01.sav"
python3 tools/madmax_save.py verify  "$SAVES/GameSave01.sav"

# what is destroyed in this save?
python3 tools/mmworld.py "$SAVES/GameSave01.sav" --status

# put every destroyed scarecrow (object type 45) back, writing to slot 7
python3 tools/mmworld.py "$SAVES/GameSave01.sav" /tmp/out.sav \
        --restore-type 45 --ref data/ladder/PT1.sav --slot 7
cp /tmp/out.sav "$SAVES/GameSave07.sav"

Turn Steam Cloud off for this game first. It will silently restore the old save over your edited one.


Repository layout

README.md                 this file
docs/
  FORMAT.md               the file format spec - start here
  FINDINGS.md             the convoy investigation, including the failures
  METHODOLOGY.md          how to run a save-diff experiment without fooling yourself
  OPEN-QUESTIONS.md       what is still unsolved, and what to try next
  GAME-FILES.md           the game's own archives - where the convoy answer came from
  ECONOMY.md              the 1520-entry roster, decoded against the game's own
                          economy table - start here for anything roster-shaped
  OBJECT-TYPES.md         per-profile counts across the 0%->100% ladder
  LOCATIONS.md            every location/activity type, with counts and evidence
  HASHES.md               the name hash - where it applies, and where it does not
  SCRIPTS.md              the game's save/load script, disassembled
tools/                    the Python utilities (see tools/README.md)
data/
  ladder/                 PT1-PT6, a 0% -> 100% reference ladder
  gibbed/                 Gibbed.MadMax's file list, 27501 game paths
  samples/                a snapshot series spanning one convoy fight
  field-map.csv           222 record ids that move across the ladder
experiments/              log of every in-game experiment and its result

The one-paragraph summary of the format

Every byte is XOR'd with a fixed repeating 32-byte key. Under that is a 0xF0 header (slot, playtime, timestamps, payload length) followed by the payload, usually an identical mirror copy of the payload, and zero padding to a 512-byte boundary. The payload has three parts: a (u32 id, u32 size, value) record stream holding story and mission state; four tables, of which the 1520-entry 24-byte one is a verbatim copy of the game's economy table (scrap piles, water, food, ammo and the threat objects) and the 13-entry 32-byte one is the convoys; and a large hash-named property store sorted by key. A u32 at offset 0 is an integrity value that the game checks — it is not solved in closed form, but it depends only on file length, so it can be carried across any length-preserving edit. Full detail in docs/FORMAT.md.


Contributing

The most useful things anyone could add:

  1. Follow the convoy graph scripts. arcx.py extracts them and adf.py decodes them into named node graphs - see docs/GAME-FILES.md. The spawner turned out to be just the map icon; convoy_choreographer.gsrc and the spawning graphs are next.
  2. The property store's key scheme. Its 64-bit keys match nothing in 101,426 real game names or 398,623 binary strings, under lookup3 or five other functions. Note this is now the only opaque keyspace left: the roster's keys turned out not to be hashes at all. See docs/HASHES.md.
  3. The other activity types. Convoys are solved, and the method that solved them is five repeatable steps (see docs/GAME-FILES.md): find the activity's .bl bundle, unpack the SARC, decode the RTPC, take the objectids of entities with save = 1, look those up in the property store. Vantage points are the best next target: CVantagePoint is a CGameObjectCreator class exactly like CConvoyDataContainer, so the method should apply unchanged. Camps are harder - their per-instance .blo files are not reachable by basename hash, so they need a staged save series instead.
  4. A closed-form solution to the integrity value. The delta-carry workaround means edits must preserve file length. Solving it properly would lift that.
  5. Camp state beyond threat. All 25 roster profiles are now labelled from the game's own data, so that job is done. What is not is the rest of a camp's state: its Threat row is strictly binary (full or zero, never partial, across 37 camps x 6 ladder saves), so the three map states and the hidden 100%-complete state live somewhere else - most likely the property store, which is where the convoy answer turned out to be. A staged capture of one camp being cleared step by step would settle it. See docs/ECONOMY.md.
  6. Windows / other-platform confirmation. Everything here was worked out on Linux under Proton. A player has confirmed a scrap edit loads correctly on Windows, which validates the decode/reseal chain including the integrity value - the failure mode that would have broken everything. Table edits and convoy resets are still unverified there; a convoy reset is the best single test, since it touches both. See docs/OPEN-QUESTIONS.md.

Please include the before/after saves with any finding. Half the wrong turns recorded in these docs would have been caught by someone else re-running the diff.

Credits

The data/ladder/ saves are "Mad Max Savegames" by Just Chill, posted as a Steam Community guide on 5 January 2018:

https://steamcommunity.com/sharedfiles/filedetails/?id=1256911456

The guide lists no restriction on rehosting. They are included unmodified apart from being flattened out of their original folder layout, and are used here only as analysis input. The type map and the convoy analysis both depend on having a clean 0%→100% reference ladder, and this one is the reason the object roster could be shown to be universal rather than per-save. Thanks to the author.

If you would rather fetch them yourself, download from the link above and drop the six GameSave01.sav files into data/ladder/ as PT1.sav … PT6.sav, in playthrough order. Nothing else needs to change.

About

Trying to build a savegame editor for the game Mad Max with the goal of resetting convoys and other parts of the game.

Resources

Stars

5 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages