The Guild Chest, first introduced in Feybreak Update, easily became the best way to move items between bases. However, it only has 54 slots and is nowhere near enough for all the items you want to store. For those who want to increase the size of the Guild Chest, especially in single player worlds, there’s a tutorial on how to do it manually: Guild chest resizer after solo play started (Manual).

A few months back, I followed this tutorial and successfully resized my Guild Chest to 270 slots. But this time on my friend’s save, the script no longer worked.

PS D:\Downloads\GuildChestResizer> python sav-to-gvas.py D:\Downloads\GuildChestResizer\Level.sav
Traceback (most recent call last):
  File "D:\Downloads\GuildChestResizer\sav-to-gvas.py", line 30, in <module>
    main()
  File "D:\Downloads\GuildChestResizer\sav-to-gvas.py", line 18, in main
    uncompressed_data = zlib.decompress(data[12:])
                        ^^^^^^^^^^^^^^^^^^^^^^^^^^
zlib.error: Error -3 while decompressing data: incorrect header check
PS D:\Downloads\GuildChestResizer>

Fortunately, I had my own save file where I performed the steps, so I could investigate the issue.

Investigation

Notice how the script tries to decompress the data using zlib, with the first 12 bytes skipped. So the first thing to check is to compare the header of the save files.

My save file (that worked):

00000000  7d d3 67 01 c8 51 23 00  50 6c 5a 32 78 9c 64 7a  |}.g..Q#.PlZ2x.dz|

My friend’s that no longer works:

00000000  09 c2 8b 01 a7 f1 20 00  50 6c 4d 31 8c 0a 00 36  |...... .PlM1...6|

According to Stack Overflow, 78 9c indicates default compression for zlib, while 8c 0a is not a valid zlib header. After playing around with the save file, I concluded that the save file was not compressed with zlib at all, so time to look for other clues.

By examining the Python script, particularly the “convert back” one (gvas-to-sav.py), I figured out the header for the .sav file:

  • Bytes 0-4: Size of uncompressed data (4 bytes, little-endian)
  • Bytes 4-8: Size of compressed data (4 bytes, little-endian)
  • Bytes 8-11: Magic number (3 bytes, PlZ)
  • Bytes 12: Possibly type information, as the script checks for 0x32

It’s easy to notice the discrepancies between the failing save file and the above structure:

  • The magic number is PlM instead of PlZ.
  • The potential type information is 0x31 instead of 0x32.

The uncompressed size and compressed size fields are correct for that file itself, so most likely the new save file employs a different compression algorithm. Time to identify it.

Finding the compression algorithm

So here comes the trick: I know about this other tool called PalEdit, which is also written in Python and is open-source on GitHub, so it’s easy to read the code for clues and insights.

There’s a SaveConverter.py file that catches my attention, which refers:

from palworld_save_tools.palsav import compress_gvas_to_sav, decompress_sav_to_gvas

… except there’s no palworld_save_tools directory anywhere in the repository. Though interestingly, there is a palworld_save_tools.zip file in the root of the repository.

Recalling how Python can execute a directory or a zip file if it contains a __main__.py script, it sure can import it as well if it contains a __init__.py, so time to unpack that ZIP archive and inspect the contents. Within a few steps you’ll land at compressor/__init__.py which looks like this at its beginning:

class SaveType:
    CNK = 0x30  # Zlib compressed on xbox
    PLM = 0x31  # Oodle compressed
    PLZ = 0x32  # Zlib compressed

# [code omitted]...

class MagicBytes:
    CNK = b"CNK"  # Zlib magic on xbox
    PLZ = b"PlZ"  # Zlib magic
    PLM = b"PlM"  # Oodle magic

Now the details become clear: The new save file contains PlM as its magic bytes and a matching 0x31 type information, so mystery solved and the next step is to figure out how to decompress it.

Patching the scripts

Searching for Oodle compression on Google produces multiple results from Epic Games and Unreal Engine, guess that’s their latest feature maybe? So I turned my focus back to PalEdit, where there’s the compressor/oozlib.py file with this:

try:
    import ooz
except ImportError:
    raise ImportError(
        f"Failed to import 'ooz' module. Make sure the Ooz library exists in {local_ooz_path} or latest pyooz is installed in your Python environment. Install using 'pip install git+https://github.com/MRHRTZ/pyooz.git'"
    )

Problem solved, but I’m on Windows without a C toolchain (I no longer do Windows development), so I moved all relevant files onto my Linux server. Then it’s just a matter of pip install command to get the requirements ready.

I also patched the decompressor script:

#!/usr/bin/env python3

import subprocess
import sys
import glob
import zlib
import ooz

def main():
    if len(sys.argv) < 2:
        exit(1)
    sav_file = sys.argv[1]
    with open(sav_file, 'rb') as f:
        data = f.read()
        uncompressed_len = int.from_bytes(data[0:4], byteorder='little')
        compressed_len = int.from_bytes(data[4:8], byteorder='little')
        magic_bytes = data[8:11]
        assert magic_bytes == b'PlM'
        save_type = data[11]
        assert save_type == 0x31
        uncompressed_data = ooz.decompress(data[12:], uncompressed_len)
        if uncompressed_len != len(uncompressed_data):
            return
        gvas_file = sav_file.replace('.sav', '.sav.gvas')
        with open(gvas_file, 'wb') as f:
            f.write(bytes(uncompressed_data))

if __name__ == "__main__":
    main()

Now it correctly decompresses my friend’s Level.sav into Level.sav.gvas where I can follow the rest of the instructions manually. For compressing the edited file back, I just used the provided gvas-to-sav.py file in the belief that the previous zlib-based compression method is still supported by the game.

After sending the edited Level.sav file back to the game, we’re glad to see our Guild Chest containing 270 slots, which we probably would never use up.

Postface

If you’re on Windows or don’t have a C/C++ compiler for installing pyooz yourself, the same palworld_save_tools.zip provides these files that you may find useful:

lib/linux_arm64/ooz.so
lib/linux_x86_64/ooz.so
lib/windows/ooz.pyd

Of course, how to utilize these files is left as an exercise for the readers.

Leave a comment