Phantom Blade Zero keeps every save slot in one file, PBZSaveGame.sav. I wanted a save editor that runs entirely in a browser tab, so nothing gets uploaded, and that meant reading the container in JavaScript. These are my notes on the format, called PBZS v3 after its magic bytes, and on the three things that cost me the most time.
The container layout builds on the MIT-licensed PBZ-Editor project and the oozextract contributors. What I added is the browser implementation and the checks described below.
Where the file lives
On Steam the save is not in %LOCALAPPDATA% like most Unreal Engine games. It sits inside the install folder:
<SteamLibrary>\steamapps\common\PBZ\ProjectPBZ\Saved\SaveGames\Steam\<17-digit SteamID>\PBZSaveGame.sav
The same folder holds 1RealTime, 2Hourly and 3Daily, which are rolling backups of the whole container.
The 12-byte header
The first 12 bytes are cleartext:
| Offset | Size | Meaning |
|---|---|---|
| 0x00 | 4 | ASCII PBZS
|
| 0x04 | 1 | Container version, 0x03
|
| 0x05 | 1 | Flags, observed as 0x01
|
| 0x06 | 2 | Slot count, uint16 LE |
| 0x08 | 4 | Size of the encrypted directory block, uint32 LE |
Sealed blocks: the tag comes first
After the header comes an encrypted directory, then one encrypted block per slot. Every sealed block has the same 36-byte preamble:
PBZS 01 01 00 00 (8 B) | nonce (12 B) | GCM tag (16 B) | ciphertext
This was the first trap. AES-GCM libraries, WebCrypto included, expect ciphertext || tag. Here the tag sits before the ciphertext, so you have to move those 16 bytes to the end before decrypting:
const nonce = block.subarray(8, 20);
const tag = block.subarray(20, 36);
const body = block.subarray(36);
const joined = new Uint8Array(body.length + 16);
joined.set(body, 0);
joined.set(tag, body.length);
const plain = await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, key, joined);
The decrypted directory is a packed list of entries:
uint16 nameLen | name (UTF-8) | uint8 algorithm | uint8 flag | uint32 rawSize | uint32 offset | uint32 blockSize
Slot names look like AutoSave0, ManualSave1 and __Advanced__. The last one holds account-wide state such as the wardrobe.
Compression: Oodle Kraken in the browser
Each slot decrypts to a compressed buffer, and the algorithm byte says how: 0 is raw JSON, 1 is zlib, 2 is Oodle Kraken. The game writes Kraken for everything.
That was the second trap, because there is no Kraken decoder in a browser. I compiled an open-source Kraken decompressor to WebAssembly. The format also defines zlib and uncompressed slots, so a writer has options. When the editor saves, every slot you did not touch keeps its original ciphertext byte for byte.
The key is a per-build constant
The AES-256 key is not in the save and is not derived from the Steam account. It is a constant in PBZ.exe: a 64-byte block that follows the UTF-16LE string sealed but block is PLAINTEXT. XOR the first 32 bytes with the last 32 and you have the key. GCM authentication of the directory block tells you whether you found the right one.
Two consequences:
- Saves are not bound to an account. The 17-digit SteamID only names the folder.
- A game update could change the key. Patch 1.0302256 (4 November 2026) did not: a save written two days after it still opens with the key from the launch build.
What is inside a slot
A slot is plain UTF-8 JSON. Inventory lives in trees such as MainWeaponInfos, ShaChiWeaponInfo, AccessoryInfos and MedicineInfos.
The third trap was mine. I first read WeaponLevel as a small "+N" grade with an offset. It is simply the level the upgrade screen shows: 1 for a new weapon, 30 at the first-playthrough cap, up to 60 in New Game Plus. A finished Cycle 10 save holds every weapon at 60. Check your assumptions against a real late-game file.
The result
The editor and the full byte-level write-up are here:
- Save editor: https://phantombladezerosaves.com/save-editor/
- Format specification: https://phantombladezerosaves.com/save-file-format/
- Spec as a repository: https://github.com/fancydirty/phantom-blade-zero-save-format
It is an unofficial fan project and is not affiliated with S-GAME. Back up the whole SteamID folder before you replace or edit anything.
Top comments (0)