Skip to main content

Reverse-engineering The Witcher 3 saves between Nintendo Switch and PC

·2009 words·10 mins
Sebastian Scheibe
Author
Sebastian Scheibe
Table of Contents

Moving a The Witcher 3: Wild Hunt save between Nintendo Switch and PC sounds as though it should be a simple file-copying exercise. Both versions describe the same game state, and CD PROJEKT RED even provides an official cross-progression system.

But a raw manual save exported from a Switch does not have the same outer format - or even the same filename - as its PC counterpart.

I compared genuine saves, tested existing converters, and built a bidirectional converter in Go to find out exactly what changes between the two platforms. The result is available as an open-source project:

Witcher 3 Switch ↔ PC Save Converter on GitHub

The implementation has only been tested with PC version 4.04 and Nintendo Switch version 4.04b. Treat other versions as untested.

The short version
#

A native manual Switch slot consists of three files with one basename:

Manual.abc12.deadbeef.123abcd.sav
Manual.abc12.deadbeef.123abcd.png
Manual.abc12.deadbeef.123abcd.req

The equivalent PC slot normally consists of two:

ManualSave_abc12_deadbeef_123abcd.sav
ManualSave_abc12_deadbeef_123abcd.png

The important findings were:

  1. A manual Switch .sav is an NXZS container holding a zlib-compressed PC save.
  2. The decompressed payload starts with the normal PC signature SNFHFZLC.
  3. Switch and PC use different filename conventions, even though the three hexadecimal identifiers are preserved.
  4. The PNG is a 228×128 thumbnail and can be copied without modification.
  5. All genuine Switch .req files I examined contained the same 27 ASCII bytes: content0;content1;content2;.
  6. The .req file is almost certainly a content-requirement manifest rather than a checksum, although its exact semantics do not appear to be publicly documented.
  7. OXZS auto and checkpoint saves are not the same format as NXZS manual saves, so the converter deliberately rejects them.

The filename detail turned out to matter just as much as the compression. A structurally correct NXZS file with a PC-style or improvised filename was rejected by the Switch. The same converted data loaded successfully on Switch 4.04b after I used the native Manual.<id>.<id>.<id> filename and supplied the complete companion set.

Starting with a genuine Switch save
#

The Switch-to-PC direction was relatively easy to establish. The open-source Python project witcher3-switch-to-pc-save-converter identifies manual Switch saves by the four-byte NXZS signature, reads two little-endian sizes, and inflates the remaining bytes with zlib. Its conversion source also verifies that the result begins with SNFHFZLC.

Comparing that implementation with genuine Switch files confirmed a 12-byte header:

Offset Size Meaning
0x00 4 bytes ASCII magic NXZS
0x04 4 bytes Decompressed size, unsigned 32-bit little-endian
0x08 4 bytes Compressed size, unsigned 32-bit little-endian
0x0C Remaining bytes RFC 1950 zlib stream

A native manual save begins roughly like this:

4e 58 5a 53  ...sizes...  78 da ...compressed data...
N  X  Z  S               zlib

The 78 DA pair is a normal zlib header commonly produced for a high-compression stream. It is not a Witcher-specific encryption marker. The container can be summarized as:

NXZS + uncompressed_size + compressed_size + zlib(PC_save)

The zlib format itself is standardized in RFC 1950 . No console key, encryption key, or proprietary decompression library is required for this manual-save envelope.

Converting Switch to PC
#

The Switch-to-PC algorithm is straightforward, but a safe implementation should do more than call a decompressor:

  1. Require at least 12 bytes.
  2. Require the NXZS magic.
  3. Read both size fields as little-endian unsigned integers.
  4. Confirm that the compressed-size field matches the number of bytes after the header.
  5. Inflate exactly one complete zlib stream.
  6. Reject checksum failures, truncated data, excessive expansion, and trailing bytes.
  7. Confirm that the decompressed length matches the declared size.
  8. Confirm that the payload starts with SNFHFZLC.
  9. Write that payload as the PC .sav.
  10. Translate the slot name from the Switch convention to the PC convention and copy its PNG.

The transformation is lossless with respect to the actual PC payload. The converter removes and validates the Switch envelope; it does not rewrite quests, inventory, world state, or any other gameplay record.

Building the reverse conversion
#

PC-to-Switch is the structural inverse:

  1. Read a PC save and require the SNFHFZLC signature.
  2. Compress the complete PC payload as a zlib stream.
  3. Write NXZS.
  4. Append the uncompressed and compressed lengths as little-endian 32-bit integers.
  5. Append the zlib stream.
  6. Translate the filename to the native Switch convention.
  7. Copy the matching 228×128 PNG.
  8. Preserve a valid .req, or generate the requirement list observed in genuine saves.

The Go implementation uses zlib’s best-compression setting. Other valid zlib levels should decompress to the same payload, but level 9 also produces the native-looking 78 DA header found in the genuine manual saves I tested.

Early hardware tests still failed even after the output had valid NXZS magic, correct sizes, level-9 zlib data, a PNG, and a .req. That initially made the compression look suspicious. The missing piece was actually the filename.

The PC form:

ManualSave_abc12_deadbeef_123abcd.sav

must become the Switch form:

Manual.abc12.deadbeef.123abcd.sav

The PNG and REQ must use the same Switch basename. Once the complete triplet used the native name, the converted save loaded successfully on a Switch running 4.04b.

This was a useful reverse-engineering reminder: file content is only one part of an on-disk format. Names, companion files, and directory layout can all be part of the loader’s contract.

What is the .req file for?
#

This is the least formally documented part of the slot.

Every genuine Switch .req I inspected - across manual and automatic slots - was exactly 27 bytes long and contained:

content0;content1;content2;

There was no newline, binary header, hash, size, timestamp, slot identifier, or save-specific value. It was simply a semicolon-terminated list of content names.

Several conclusions follow directly from the bytes:

  • It is not a checksum of the .sav; unrelated saves had identical REQ data.
  • It is not compression metadata; the compressed and decompressed sizes already live in the NXZS header.
  • It is not thumbnail metadata; the PNG is independently decodable.
  • It is not a unique save identifier; none of the filename identifiers appear in it.

The strongest interpretation is that .req means requirements: it records the game-content packages that must be available before the slot can be loaded. The names align with The Witcher 3’s content0, content1, and content2 package or directory naming. This also fits the game’s behavior when a save requires content or expansions that are absent on the destination system.

That interpretation remains an inference. I could not find an official CD PROJEKT RED specification for the .req grammar or a definitive mapping from those tokens to individual expansions. It would therefore be misleading to claim, for example, that one particular token means Hearts of Stone or Blood and Wine.

There is one useful official contrast: CD PROJEKT RED’s PC support form describes a PC save as a same-named .sav and .png pair. It does not request .req on PC. That matches the files observed here: .req is native Switch-side slot metadata, while an ordinary PC slot works as a SAV/PNG pair.

Can the Switch load a slot without .req? My samples prove that the game creates one, and preserving or generating it reproduces the native layout. They do not isolate .req as the sole cause of success or failure because filename correctness was also essential. The conservative behavior is therefore to require a valid REQ for Switch input and always produce one for Switch output.

For the tested Complete Edition setup, the generated value is:

content0;content1;content2;

For other editions or future versions, copying the REQ from a genuine compatible slot is safer than assuming this list is universal.

The PNG is part of the slot too
#

Both genuine Switch and PC thumbnails examined here were valid 228×128 PNG images. They appear in the game’s load menu and do not need conversion.

The converter validates the image rather than accepting any file with a .png suffix. PC output requires a same-named SAV/PNG pair, consistent with CD PROJEKT RED’s support documentation. Switch output requires the native SAV/PNG/REQ triplet.

Why OXZS is not handled as NXZS
#

Genuine Switch exports can also contain auto and checkpoint saves beginning with OXZS. Their bytes after the outer header do not form the same ordinary zlib stream found at offset 0x0C in an NXZS manual save.

Treating OXZS as a spelling variation of NXZS would risk emitting corrupted data. The project recognizes the signature but rejects it with a specific message. For now, the reliable workflow is to create a manual save on Switch and convert that NXZS slot.

Comparing another bidirectional converter
#

I also reviewed NS-PC-Save-Transfer , which advertises bidirectional Witcher 3 conversion. At the commit inspected during this analysis, its Witcher 3 plugin attempted inflation from offset 0x0C for Switch-to-PC, which is consistent with the NXZS layout.

Its reverse path, however, copied the PC save and patched one byte at offset 0x0C16; it did not construct an NXZS container, translate native slot names, or generate .req. That did not match the genuine 4.04b Switch files used in this investigation, so I did not use it as the model for the Go implementation.

The comparison reinforced a broader point: a tool’s direction label is not enough. The emitted bytes and the complete slot layout need to be compared with files produced by the target game.

Validation matters as much as conversion
#

The finished Go tool validates complete slots rather than merely checking four magic bytes.

For PC it checks:

  • The native ManualSave_<5 hex>_<8 hex>_<7 hex>.sav filename.
  • The SNFHFZLC payload signature.
  • A same-named, decodable 228×128 PNG.

For Switch it checks:

  • The native Manual.<5 hex>.<8 hex>.<7 hex>.sav filename.
  • A same-named, decodable 228×128 PNG.
  • A same-named REQ containing a consecutive, semicolon-terminated content list beginning at content0.
  • NXZS magic and both declared sizes.
  • Complete zlib integrity, including its checksum and rejection of trailing data.
  • The decompressed length and inner SNFHFZLC signature.

Conversion performs the same preflight on the complete slot. An incomplete or invalid source is skipped before an output .sav is written, and the command returns a non-zero status if any slot fails. A folder can contain multiple save slots; valid slots are processed independently without recursively walking unrelated subfolders.

Using the open-source converter
#

The project publishes Windows, Linux, Intel Mac, and Apple Silicon Mac builds through GitHub Releases:

Download the latest release

After extracting it, conversion looks like this on macOS or Linux:

./w3save pc-to-switch --input "pc-saves" --output "switch-saves"
./w3save switch-to-pc --input "switch-saves" --output "pc-saves"

Validation accepts either one .sav path or a folder:

./w3save validate --input "switch-saves" --format switch
./w3save validate --input "pc-saves/ManualSave_abc12_deadbeef_123abcd.sav"

On Windows PowerShell, use .\w3save.exe in place of ./w3save.

Always back up the original PC save folder and Switch export before restoring converted files.

Scope and limitations
#

This project converts an outer storage representation. It cannot make fundamentally incompatible game states compatible.

  • Testing is limited to PC 4.04 and Switch 4.04b.
  • Manual NXZS saves are supported; OXZS auto and checkpoint saves are not.
  • Matching editions and installed content still matter.
  • The destination game should be at least as new as the source game.
  • The tool does not bypass ownership, encryption, console security, or copy protection.
  • No game assets or example player saves are distributed with the project.

When it is available, CD PROJEKT RED’s official cross-progression system remains the easiest supported route. The local converter is useful when cloud transfer is unavailable, for examining backups, or for players who prefer an auditable offline process.

Conclusion
#

The core save data was never the hardest part. A manual Switch save is a compact, understandable NXZS wrapper around the ordinary PC payload. The tricky part was reproducing the entire native slot: container, sizes, compression, filename, PNG, and REQ metadata.

The .req file is best understood as a small required-content manifest. Its observed contents are simple, but its exact token-to-expansion mapping remains undocumented. Treating that uncertainty honestly - and validating the complete slot around it - makes the converter safer than relying on a magic-byte check alone.

The complete Go source, tests, validation rules, and downloadable builds are available in the GitHub repository .

Featured image: official Nintendo Switch promotional artwork for The Witcher 3: Wild Hunt - Complete Edition, via Bandai Namco Europe .

This is an independent open-source project and is not affiliated with or endorsed by CD PROJEKT RED or Nintendo.