Inside Phantom Blade Zero's PBZS v3 Save Format (AES-256-GCM + Oodle Kraken in the Browser)

2026-11-03来源 dev.toEnglish

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\, and 3Daily\ — 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 — Standard zlib deflate 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.Dust holds your currency balance as a standard signed integer.
  • Inventory.InventoryInfos maps material and goods IDs (such as Weapon_Material_* and Medi_Material_*) to { CurrentAmount, MaxAmount }.
  • Inventory.MainWeaponInfos and Inventory.ShaChiWeaponInfo store 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):

原文:https://www.a8q9.cn/en/article/inside-phantom-blade-zero-s-pbzs-v3-save-format-aes-256-gcm-oodle-kraken-in-the--d7c0ec.html

相关文章