Most Unreal Engine 5 action RPGs on PC drop your save files into %LOCALAPPDATA%\<ProjectName>\Saved\SaveGames\. If you go looking there for Phantom Blade Zero, all you'll find is GameUserSettings.ini and shader caches.
While building a client-side browser save editor and starter-save archive at phantombladezerosaves.com, we had to document where the Steam build actually writes PBZSaveGame.sav and how its custom PBZS v3 binary container seals and compresses slot records. Here is a breakdown of the format and the two quirks that trip up standard WebCrypto and compression libraries.
1. Where Steam Actually Stores PBZSaveGame.sav
Instead of your Windows user profile, Phantom Blade Zero writes saves directly inside the Steam library folder next to the game installation:
<SteamLibrary>\steamapps\common\PBZ\ProjectPBZ\Saved\SaveGames\Steam\<17-digit-SteamID64>\PBZSaveGame.sav Inside that 17-digit folder sit four items:
-
PBZSaveGame.sav— a single archive holding all manual slots, autosaves, and your account wardrobe. -
1RealTime\,2Hourly\, and3Daily\— automatic rolling backup directories rotated by the game while you play.
One useful discovery from decrypting the container: neither the outer directory nor the inner slot JSON stores your 17-digit SteamID64. The number only names the folder on disk. That means any valid PBZSaveGame.sav can be dropped into your own SteamID64 folder and loaded at the Bell checkpoint without re-signing an account ID (see our Steam save location & backup guide and account transfer walkthrough for the Steam Cloud sync ordering).
2. The 12-Byte PBZS v3 Header
Only the first 12 bytes of PBZSaveGame.sav are stored in cleartext:
| Offset | Size | Type | Meaning |
|---|---|---|---|
0x00 | 4 B | ASCII | Magic bytes PBZS (50 42 5A 53) |
0x04 | 1 B | uint8 | Container version (3 in Steam build 26266538) |
0x05 | 1 B | uint8 | Flags (1) |
0x06 | 2 B | uint16 LE | Total number of slot records in the archive |
0x08 | 4 B | uint32 LE | Byte length of the encrypted directory block starting at offset 12 |
Because the header gives you both slotCount and directorySize, you can verify whether a file is an intact PBZS v3 archive without having the decryption key: 12 + directorySize must land on a sealed block boundary, and walking slotCount blocks from there must reach EOF with zero trailing bytes.
3. Quirk #1: Out-of-Line AES-256-GCM Authentication Tags
Immediately after byte 12 sits the directory block, followed by one sealed block per save slot (__Advanced__, __Achievements__, AutoSave0, ManualSave1, NightmareRun0, etc.). Every sealed block shares a 36-byte preamble:
PBZS 01 01 00 00 (8 B) | nonce (12 B) | GCM tag (16 B) | ciphertext (N B) Most crypto APIs — including browser WebCrypto (crypto.subtle.decrypt) — expect the 16-byte AES-GCM authentication tag to be appended to the end of the ciphertext (ciphertext || tag). PBZS v3 stores the 16-byte tag before the ciphertext at bytes 20..36.
To decrypt a block in the browser with WebCrypto, you slice the 12-byte nonce from 8..20, copy ciphertext (36..end) followed by tag (20..36) into a contiguous Uint8Array, and pass that buffer to crypto.subtle.decrypt({ name: 'AES-GCM', iv: nonce, tagLength: 128 }, key, combined).
If the key is wrong or a single byte was corrupted in transit, GCM authentication fails immediately before touching the payload.
4. Quirk #2: Oodle Kraken Slot Payloads vs. zlib Re-Encoding
Decrypting the directory block reveals a packed table of slot entries:
uint16 nameLen | name (UTF-8) | uint8 algorithm | uint8 flag | uint32 rawSize | uint32 offset | uint32 blockSize The algorithm byte specifies how each slot's UTF-8 JSON was compressed prior to encryption:
-
0— Uncompressed raw bytes -
1— Standardzlibdeflate stream -
2— Oodle Kraken
Every native save written by the game uses algorithm = 2 (Oodle Kraken), but the game's loader natively accepts algorithm = 1 (zlib) as well. That makes a pure browser editor practical: we ship a compiled WebAssembly Oodle Kraken decoder (ooz.wasm) to decompress native algorithm = 2 slots in the tab, modify only the touched JSON keys, and re-pack modified slots with zlib while copying untouched slots through with their original ciphertext byte-for-byte intact.
Full byte-level notes live in the PBZS v3 Format Specification and the open-source verifier repo at github.com/fancydirty/phantom-blade-zero-save-format.
5. Inside the Slot JSON: The +1 Weapon Grade Offset
Once a slot is decompressed, the payload is clean UTF-8 JSON:
-
Player.Dustholds your currency balance as a standard signed integer. -
Inventory.InventoryInfosmaps material and goods IDs (such asWeapon_Material_*andMedi_Material_*) to{ CurrentAmount, MaxAmount }. -
Inventory.MainWeaponInfosandInventory.ShaChiWeaponInfostore weapon ownership and upgrade levels.
One detail that catches manual JSON editors off guard: weapon grades are stored one higher than the +n shown in the game's UI. An un-upgraded base weapon (+0) is stored as 1 (or omitted), and the first-cycle ceiling of +7 is stored as 8. Outfits are stored globally inside the __Advanced__ slot rather than inside individual manual saves. We published the full mapping of internal string keys to English and Chinese names on the Phantom Blade Zero Item IDs page.
Try the Local Browser Save Editor or Starter Saves
If you want to inspect or edit your own PBZSaveGame.sav without installing desktop trainers or Cheat Engine tables (which frequently break when a Steam hotfix shifts memory pointers — see our Save Editor vs. Trainers comparison):
- Phantom Blade Zero Browser Save Editor — runs 100% locally in your browser tab via WebCrypto and WASM; no files are ever uploaded.
- Cycle 1 All-Weapons Starter Save, Cycle 1 Max Dust & Materials Save, and Cycle 10 & 66 Days Mode Save — decrypted, verified against Steam build
26266538, and published with SHA-256 hashes.

