Minion Rush Windows Store Edition save data editor (AI generated) A developer released version 1.2.0 of a Python-based save data editor for the Windows Store edition of Minion Rush. The tool rebuilds savegame files from JSON, using XTEA encryption and patching wallet shadow values to ensure consistency. It supports custom keys and connectivity trackers, offering a practical utility for modding the game. | /usr/bin/env python3 | | """ | | Minion Rush savegame encoder, version 1.2.0. | | | | Rebuilds a Windows Store Minion Rush savegame from JSON produced by | | minion save decoder v2 modified.py. | | | | A template savegame is required because the decoded JSON intentionally does not | | contain every opaque RedundantStream marker byte. The encoder preserves the | | physical wrapper and replaces each valid redundant payload copy in-place. | | | | Typical use: | | python minion save encoder v1.py savegame.json --template savegame --out savegame.new | | | | The top-level decoded values object is applied to the Player and MapMgr blobs. | | Optionally, the same banana/token values can also be written to a raw | | connectivity trackers file using --connectivity-template/--connectivity-out. | | | | Important: version-0x1C Player data contains an on-disk wallet shadow near the | | end of the serialized blob. When its valid flag is 1, the deserializer uses the | | shadow values to overwrite the ordinary banana/token fields. This encoder patches | | both representations together. | | | | Fields: | | banana count - Player + 0x10 u32 little-endian | | token count - Player + 0x0C u32 little-endian | | current jelly lab level - MapMgr + 0x08 u32 little-endian | | """ | | | | from future import annotations | | | | import argparse | | import base64 | | import copy | | import json | | import math | | import struct | | import sys | | import zlib | | from pathlib import Path | | from typing import Any | | | | HEADER MARKER SIZE = 0xB0 | | HEADER SIZE = HEADER MARKER SIZE + 8 | | PAYLOAD MARKER SIZE = 0x90 | | SAVE MAGIC = 0xED | | DEFAULT KEY SOURCE = b"ERROR: invalid stream" | | MAX U32 = 0xFFFFFFFF | | VERSION = "1.2.0" | | | | | | class EncodeError Exception : | | pass | | | | | | def derive xtea key source: bytes - bytes: | | key = bytearray 16 | | for i, byte in enumerate source : | | key i & 0x0F ^= byte | | return bytes key | | | | | | def xtea encrypt block block: bytes, key: bytes - bytes: | | if len block = 8: | | raise ValueError "XTEA block must be exactly 8 bytes" | | if len key = 16: | | raise ValueError "XTEA key must be exactly 16 bytes" | | | | v0, v1 = struct.unpack "<2I", block | | k = struct.unpack "<4I", key | | delta = 0x9E3779B9 | | total = 0 | | | | for in range 32 : | | v0 = | | v0 | | + | | v1 << 4 ^ v1 5 + v1 | | ^ total + k total & 3 & MAX U32 | | | | & MAX U32 | | total = total + delta & MAX U32 | | v1 = | | v1 | | + | | v0 << 4 ^ v0 5 + v0 | | ^ total + k total 11 & 3 & MAX U32 | | | | & MAX U32 | | | | return struct.pack "<2I", v0, v1 | | | | | | def xtea encrypt data: bytes, key: bytes - bytes: | | if len data % 8: | | raise EncodeError "XTEA plaintext must be padded to a multiple of 8 bytes" | | return b"".join | | xtea encrypt block data offset:offset + 8 , key | | for offset in range 0, len data , 8 | | | | | | | | def parse key args: argparse.Namespace - bytes: | | supplied = sum | | value is not None | | for value in args.key source, args.key source hex, args.xtea key hex | | | | if supplied 1: | | raise EncodeError | | "use only one of --key-source, --key-source-hex, or --xtea-key-hex" | | | | | | if args.xtea key hex is not None: | | try: | | key = bytes.fromhex args.xtea key hex | | except ValueError as exc: | | raise EncodeError f"invalid --xtea-key-hex: {exc}" from exc | | if len key = 16: | | raise EncodeError "--xtea-key-hex must decode to exactly 16 bytes" | | return key | | | | if args.key source hex is not None: | | try: | | source = bytes.fromhex args.key source hex | | except ValueError as exc: | | raise EncodeError f"invalid --key-source-hex: {exc}" from exc | | return derive xtea key source | | | | if args.key source is not None: | | return derive xtea key args.key source.encode args.key encoding | | | | return derive xtea key DEFAULT KEY SOURCE | | | | | | def checked u32 value: Any, name: str - int: | | if isinstance value, bool or not isinstance value, int : | | raise EncodeError f"{name} must be an integer" | | if not 0 <= value <= MAX U32: | | raise EncodeError f"{name} must be in the range 0..{MAX U32}" | | return value | | | | | | | | def rol32 value: int, count: int - int: | | count &= 31 | | value &= MAX U32 | | return value << count | value 32 - count & 31 & MAX U32 | | | | | | def wallet shadow encode value: int - int: | | return rol32 checked u32 value, "wallet shadow value" , 10 ^ 0x48607922 | | | | | | def patch wallet shadow | | blob: bytearray, | | old tokens: int, | | old bananas: int, | | new tokens: int, | | new bananas: int, | | - dict str, Any : | | """Patch the serialized wallet shadow used by Player format 0x1C. | | | | Serialized layout is contiguous: | | uint8 valid flag == 1 | | uint32 token shadow | | uint32 banana shadow | | | | shadow value = ROL32 value, 10 XOR 0x48607922. | | """ | | old seq = | | b"\x01" | | + struct.pack "