Skip to content

lbrooney/BTD6-Save-Converter

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BloonsTD6 Save Converter

Convert BloonsTD6+ (Apple Arcade) save files so they can be loaded by the Steam version of Bloons Tower Defense 6. Conversion in the other direction (Steam → Apple Arcade) is also supported (although it is not required as the Apple Arcade version can load Steam saves).

Web app — convert in your browser with no setup required.

Background

The Apple Arcade version can load Steam saves natively, but the Steam version rejects Apple Arcade saves. This is because the save file embeds a platform identifier (savedBySkuId) that the Steam client checks and refuses if it doesn't match. This tool decrypts the save, patches the relevant fields, and re-encrypts it in a format the Steam client accepts.

See REVERSE_ENGINEERING.md for a full account of how the save format was figured out.

Requirements

The script uses uv inline script metadata to declare its own dependency (pycryptodome), so no manual setup is needed if you have uv installed.

With uv (recommended):

uv run convert.py <command> ...

Without uv, install the dependency manually then run with Python 3.10+:

pip install "pycryptodome>=3.23.0"
python3 convert.py <command> ...

Usage

Convert Apple Arcade → Steam

Takes your Apple Arcade save and produces a file the Steam version will load:

uv run convert.py to-steam BloonsTD6+.Save output.Save

The output will have the Steam platform ID in its header and savedBySkuId set to 1136 (Steam's SKU). All gameplay progress — XP, monkey money, unlocks, etc. — is preserved exactly.

Convert Steam → Apple Arcade

uv run convert.py to-arcade BloonsSteam.Save output.Save

Inspect a save file

Prints a summary of a save without modifying it:

uv run convert.py inspect BloonsTD6+.Save

Example output:

Inspecting: BloonsTD6+.Save
  Header platform ID : 95 (Apple Arcade)
  savedBySkuId       : 1108 (Apple Arcade)
  savedByGameVersion : 53.2
  rank               : 48.0
  xp                 : 1598108.0
  monkeyMoney        : 3708.0
  ownerID            : 69bdbad81ab8d0727b4fcb42
  timeStamp          : 2026-03-27T19:49:05.896705-07:00
  JSON size          : 83,611 bytes

Decrypt to JSON

Decrypts a save file to a human-readable JSON file for manual inspection or editing:

uv run convert.py decrypt BloonsTD6+.Save save.json

Encrypt JSON back to a save

Re-encrypts a JSON file (previously produced by decrypt) back into a .Save file. Use --platform to set the target platform:

uv run convert.py encrypt save.json Profile.Save --platform steam
uv run convert.py encrypt save.json Profile.Save --platform arcade

Where to find your save file

Platform Save file location
Steam (macOS) ~/Library/Application Support/NinjaKiwi/BTD6/Profile.Save
Steam (Windows) %APPDATA%\NinjaKiwi\BTD6\Profile.Save
Apple Arcade (macOS) ~/Library/Containers/com.ninjakiwi.bloonstd6plus/Data/Library/Application Support/com.ninjakiwi.bloonstd6plus/link/PRODUCTION/<SaveID>/Profile.Save

Back up your save before converting. The converter writes a new file and never modifies the original.

What changes during conversion

to-steam makes three targeted changes to the JSON payload; everything else is left untouched:

Field Apple Arcade value Steam value
savedBySkuId 1108 1136
pendingSteamAttribution null "" (empty string)
trophiesWalletId UUID string null

The header's platform ID (bytes 8–11) is also updated from 95 to 18. A new random salt is generated for re-encryption, so the output bytes are never identical to the input even if the JSON is unchanged.


REVERSE_ENGINEERING.md

REVERSE_ENGINEERING.md documents the full process of figuring out the save format from scratch, starting with no prior knowledge. It covers:

  • Initial reconnaissance — comparing the two save files byte-by-byte and parsing the header as structured integers to spot the platform ID difference at offset 8.
  • Entropy and block-alignment analysis — using Shannon entropy (~8.0) to confirm the payload is encrypted, and testing AES block alignment to determine that the header is exactly 44 bytes.
  • Game engine identification — recognising the Unity + IL2CPP setup from the bundle layout and understanding what that means: the game logic is compiled native code in GameAssembly.dylib, but all class and method names are preserved in a separate symbol table (global-metadata.dat).
  • IL2CPP metadata mining — using strings on global-metadata.dat as a symbol table to discover the save codec class hierarchy: EncryptedFileReader, EncryptedFileWriter, PlayerSavePasswordGenerator, and the compile-time constants PBKDF2_ITERATIONS, PBKDF2_SALT_BITS, AES_KEY_BITS, ZLIB_LEVEL, and so on.
  • Encryption stack reconstruction — tracing the discovery of RijndaelManaged (AES), Rfc2898DeriveBytes (PBKDF2), and CryptoStream to confirm the full encryption pipeline.
  • Finding the password — why the password "11" could not be extracted directly from the binary, and how cross-referencing with open-source community tools (monke, NKSave) confirmed it.
  • Decryption verification — the Python code that successfully decrypted both saves to JSON, proving the format was fully understood.
  • Platform difference analysis — a systematic JSON diff of the two saves identifying savedBySkuId as the primary compatibility gate.
  • Implementation notes — annotated code for each stage of the pipeline (PBKDF2 derivation, AES-CBC, PKCS#7 padding, zlib), plus notes on JSON serialisation fidelity (BOM preservation, separator style).
  • Complete file format reference — a byte-map table, all encryption parameters, and a "key insight chain" that summarises the logical steps from raw bytes to working decryption.

About

Convert BloonsTD6+ (Apple Arcade) save files so they can be loaded by the Steam version of Bloons Tower Defense 6. Conversion in the other direction (Steam → Apple Arcade) is also supported.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages