[ Beneath the Waves ]

Returnal Unpacker

article by Ben Lincoln


Table of contents

  1. Introduction
  2. Usage
  3. Building from Source
  4. Can I Do Anything with the Unpacked Files?
  5. Technical Notes
  6. Side Discussion: Bugs in Ooz / libooz
  7. Version History


This is a modified version of rust-u4pak by Mathias Panzenböck (AKA panzi) that can unpack the Unreal Engine PAK files from the PC version of Returnal. It probably won't work with PAK files from any other game, but it does successfully unpack everything from Returnal. It does not currently support packing the files, so is mostly useful as a way to explore the content inside, not necessarily modify the game.

In addition to Mathias Panzenböck's rust-u4pak project, this utility also incorporates code from the following:

A big thank you to all of the authors involved in those projects!

FModel can also be used to unpack Returnal's PAK files, and has a GUI if one prefers. It can even convert some of the data inside, like textures.

This page will only discuss the tool itself. I'll be placing information that I find with it in the Returnal article.


u4pakreturnal.exe" unpack -o "<base output directory>" -t 1 "<path to input PAK file>"


u4pakreturnal.exe" unpack -o "C:\Users\blincoln\Documents\Returnal-Extracted\pakchunk0" -t 1 "C:\Program Files (x86)\Steam\SteamApps\common\Returnal\Returnal\Content\Paks\pakchunk0-WindowsNoEditor.pak"

There are 19 PAK files included with the initial release of Returnal for the PC, located in C:\Program Files (x86)\Steam\SteamApps\common\Returnal\Returnal\Content\Paks by default. You will need at least 100 GiB of disk space to decompress all of them. The download package contains a utility script named unpack_all.bat to simplify this. e.g.:

unpack_all.bat "C:\Program Files (x86)\Steam\SteamApps\common\Returnal\Returnal\Content\Paks" "C:\Users\blincoln\Documents\Returnal-Extracted"

Building from Source

I do most of my technical work on Linux these days, so I built u4pakreturnal.exe in a Linux VM with the standard Rust development tools (including the x86_64-pc-windows-gnu target) as well as MinGW installed. In that environment, you should only need to run one command to build the Windows binary:

cargo build --target="x86_64-pc-windows-gnu"

It seems like it should be possible to build a working Linux binary as well, but I haven't needed one yet, and I know it doesn't work with the existing source code.

Can I Do Anything with the Unpacked Files?

Some of the content is human-readable - text files for things like subtitles, configuration, descriptions, and so forth. Some of the audio is in Ogg Vorbis format, so you can open that in tools like Audacity.

I haven't had time to figure out if any of the standard Unreal tools will open the models, textures, etc. I'm sure there's a way to do it, I just don't know how much work it will be for someone.

Technical Notes

Returnal's PAK files use two types of compression; zlib, and Oodle. rust-u4pak already supported zlib, but Returnal uses a compression type of 2 to indicate zlib, instead of 1 like (most?) other Unreal Engine titles. A compression type of 1 in Returnal indicates that the file is compressed using RAD Game Tools' proprietary Oodle compression. Public documentation on that format is not ideal at the time of this writing, so it took a decent amount of effort for me to even figure out that it was using Oodle instead of LZ4. The game binaries refer to LZ4 and zlib all over the place, but LZ4 doesn't seem to be used for the PAK chunks at all.

In the Returnal PAK files, at least, all of the Oodle data begins with the two-byte header 0x8C0C. This magic number of 0x0C8C (or 3212 in decimal) seems to be associated specifically with the "Leviathan" variation of Oodle, if I'm reading the output generated by Ooz correctly. Here's a table of the Ooz output in case it makes it easier for people to discover that their mystery data was compressed using Oodle:

Header (Little-Endian) Header (Big-Endian) Header (Decimal) Suspected Type of Oodle Compression
0x8C06 0x068C 1676 Kraken
0x8C0A 0x0A8C 2700 Selkie or Mermaid
0x8C0C 0x0C8C 3212 Leviathan

Oodle has another compression mode named Hydra, but I couldn't get Ooz to compress using that mode in my brief testing.

Side Discussion: Bugs in Ooz / libooz

Ooz is a really neat project. If it didn't exist, figuring out how to decompress the Oodle data would have taken much, much longer.

However, it is also very buggy. When you run the standalone binary compressor/decompressor, part of the output is "Warning! not fuzz safe, so please trust the input", and the authors are not kidding.

Unfortunately, all of the commonly-used branches of Ooz on GitHub have issue-tracking disabled, and I didn't see a good way to contact the authors otherwise. So here are a list of the issues I worked around in the customized version of Ooz used for this project.

When using the library, callers need to pass an output buffer that is larger than the expected decompressed data

When I first hacked rust-u4pak to use libooz, it would successfully decompress a decent number of files and then segfault. The command-line ooz.exe would decompress the same data correctly. Examining the source, I discovered this:

// The decompressor will write outside of the target buffer.

#define SAFE_SPACE 64

When allocating the output buffer, the command-line code for Ooz refers to this extra space:

output = new byte[unpacked_size + SAFE_SPACE];

IMO it would make more sense for the decompressor to allocate a buffer that was big enough, write the data there, then copy the expected amount to the actual output buffer, but I'm not a world-class C/C++ developer. In the interest of time, I implemented the same workaround in u4pakreturnal.exe, except that a much more generous amount of space is allocated.

Assumptions about the number of arrays/dictionaries in the compressed data

Older versions of Ooz assumed there would never be more than 32 arrays/dictionaries:

uint8* entropy_array_data[32];

uint32 entropy_array_size[32];

...or, in more recent versions, 63:

uint8 *entropy_array_data[63];

uint32 entropy_array_size[63];

This caused a completely different type of segmentation fault due to buffer overflow, because many of the files in Returnal have 35 or more arrays/dictionaries. I'm not sure if there is a hard limit of 63 in the Oodle format itself, or that's just a guess on the part of the Ooz authors. The workarounds I've implemented seem to be enough for Returnal.

Version History

In reverse chronological order:

Version 1.1 - released 2023-02-27

Version 1.0 - released 2023-02-24


File Size Version Release Date Author
u4pakreturnal 19 MiB 1.1 2023-02-27 various
File Size Version Release Date Author
u4pakreturnal - source code 211 KiB 1.1 2023-02-27 various
File Size Version Release Date Author
u4pakreturnal 19 MiB 1.0 2023-02-24 various
File Size Version Release Date Author
u4pakreturnal - source code 211 KiB 1.0 2023-02-24 various
[ Page Icon ]