|
| 1 | +--- |
| 2 | +title: "Manually resize Guild Chest in Palworld 0.6" |
| 3 | +tags: games palworld |
| 4 | +redirect_from: /p/77 |
| 5 | +--- |
| 6 | + |
| 7 | +The Guild Chest, first introduced in Feybreak Update, easily became the best way to move items between bases. |
| 8 | +However, it only has 54 slots and is nowhere near enough for all the items you want to store. |
| 9 | +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: |
| 10 | +[Guild chest resizer after solo play started (Manual)](https://www.nexusmods.com/palworld/mods/2419). |
| 11 | + |
| 12 | +A few months back, I followed this tutorial and successfully resized my Guild Chest to 270 slots. |
| 13 | +But this time on my friend's save, the script no longer worked. |
| 14 | + |
| 15 | +```console |
| 16 | +PS D:\Downloads\GuildChestResizer> python sav-to-gvas.py D:\Downloads\GuildChestResizer\Level.sav |
| 17 | +Traceback (most recent call last): |
| 18 | + File "D:\Downloads\GuildChestResizer\sav-to-gvas.py", line 30, in <module> |
| 19 | + main() |
| 20 | + File "D:\Downloads\GuildChestResizer\sav-to-gvas.py", line 18, in main |
| 21 | + uncompressed_data = zlib.decompress(data[12:]) |
| 22 | + ^^^^^^^^^^^^^^^^^^^^^^^^^^ |
| 23 | +zlib.error: Error -3 while decompressing data: incorrect header check |
| 24 | +PS D:\Downloads\GuildChestResizer> |
| 25 | +``` |
| 26 | + |
| 27 | +Fortunately, I had my own save file where I performed the steps, so I could investigate the issue. |
| 28 | + |
| 29 | +## Investigation |
| 30 | + |
| 31 | +Notice how the script tries to decompress the data using zlib, with the first 12 bytes skipped. |
| 32 | +So the first thing to check is to compare the header of the save files. |
| 33 | + |
| 34 | +My save file (that worked): |
| 35 | + |
| 36 | +```text |
| 37 | +00000000 7d d3 67 01 c8 51 23 00 50 6c 5a 32 78 9c 64 7a |}.g..Q#.PlZ2x.dz| |
| 38 | +``` |
| 39 | + |
| 40 | +My friend's that no longer works: |
| 41 | + |
| 42 | +```text |
| 43 | +00000000 09 c2 8b 01 a7 f1 20 00 50 6c 4d 31 8c 0a 00 36 |...... .PlM1...6| |
| 44 | +``` |
| 45 | + |
| 46 | +According to [Stack Overflow](https://stackoverflow.com/q/9050260/5958455), `78 9c` indicates default compression for zlib, while `8c 0a` is not a valid zlib header. |
| 47 | +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. |
| 48 | + |
| 49 | +By examining the Python script, particularly the "convert back" one (`gvas-to-sav.py`), I figured out the header for the `.sav` file: |
| 50 | + |
| 51 | +- Bytes 0-4: Size of uncompressed data (4 bytes, little-endian) |
| 52 | +- Bytes 4-8: Size of compressed data (4 bytes, little-endian) |
| 53 | +- Bytes 8-11: Magic number (3 bytes, `PlZ`) |
| 54 | +- Bytes 12: Possibly type information, as the script checks for `0x32` |
| 55 | + |
| 56 | +It's easy to notice the discrepancies between the failing save file and the above structure: |
| 57 | + |
| 58 | +- The magic number is `PlM` instead of `PlZ`. |
| 59 | +- The potential type information is `0x31` instead of `0x32`. |
| 60 | + |
| 61 | +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. |
| 62 | +Time to identify it. |
| 63 | + |
| 64 | +## Finding the compression algorithm |
| 65 | + |
| 66 | +So here comes the trick: I know about this other tool called [PalEdit](https://www.nexusmods.com/palworld/mods/104), which is also written in Python and is [open-source on GitHub](https://github.com/EternalWraith/PalEdit), so it's easy to read the code for clues and insights. |
| 67 | + |
| 68 | +There's a `SaveConverter.py` file that catches my attention, which refers: |
| 69 | + |
| 70 | +```python |
| 71 | +from palworld_save_tools.palsav import compress_gvas_to_sav, decompress_sav_to_gvas |
| 72 | +``` |
| 73 | + |
| 74 | +... except there's no `palworld_save_tools` directory anywhere in the repository. |
| 75 | +Though interestingly, there *is* a [`palworld_save_tools.zip` file](https://github.com/EternalWraith/PalEdit/blob/main/palworld_save_tools.zip) in the root of the repository. |
| 76 | + |
| 77 | +Recalling how Python can [execute](https://docs.python.org/3/using/cmdline.html#interface-options) 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. |
| 78 | +Within a few steps you'll land at `compressor/__init__.py` which looks like this at its beginning: |
| 79 | + |
| 80 | +```python |
| 81 | +class SaveType: |
| 82 | + CNK = 0x30 # Zlib compressed on xbox |
| 83 | + PLM = 0x31 # Oodle compressed |
| 84 | + PLZ = 0x32 # Zlib compressed |
| 85 | + |
| 86 | +# [code omitted]... |
| 87 | + |
| 88 | +class MagicBytes: |
| 89 | + CNK = b"CNK" # Zlib magic on xbox |
| 90 | + PLZ = b"PlZ" # Zlib magic |
| 91 | + PLM = b"PlM" # Oodle magic |
| 92 | +``` |
| 93 | + |
| 94 | +Now the details become clear: |
| 95 | +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. |
| 96 | + |
| 97 | +## Patching the scripts |
| 98 | + |
| 99 | +Searching for `Oodle compression` on Google produces multiple results from Epic Games and Unreal Engine, guess that's their latest feature maybe? |
| 100 | +So I turned my focus back to PalEdit, where there's the `compressor/oozlib.py` file with this: |
| 101 | + |
| 102 | +```python |
| 103 | +try: |
| 104 | + import ooz |
| 105 | +except ImportError: |
| 106 | + raise ImportError( |
| 107 | + 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'" |
| 108 | + ) |
| 109 | +``` |
| 110 | + |
| 111 | +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. |
| 112 | +Then it's just a matter of `pip install` command to get the requirements ready. |
| 113 | + |
| 114 | +I also patched the decompressor script: |
| 115 | + |
| 116 | +```python |
| 117 | +#!/usr/bin/env python3 |
| 118 | + |
| 119 | +import subprocess |
| 120 | +import sys |
| 121 | +import glob |
| 122 | +import zlib |
| 123 | +import ooz |
| 124 | + |
| 125 | +def main(): |
| 126 | + if len(sys.argv) < 2: |
| 127 | + exit(1) |
| 128 | + sav_file = sys.argv[1] |
| 129 | + with open(sav_file, 'rb') as f: |
| 130 | + data = f.read() |
| 131 | + uncompressed_len = int.from_bytes(data[0:4], byteorder='little') |
| 132 | + compressed_len = int.from_bytes(data[4:8], byteorder='little') |
| 133 | + magic_bytes = data[8:11] |
| 134 | + assert magic_bytes == b'PlM' |
| 135 | + save_type = data[11] |
| 136 | + assert save_type == 0x31 |
| 137 | + uncompressed_data = ooz.decompress(data[12:], uncompressed_len) |
| 138 | + if uncompressed_len != len(uncompressed_data): |
| 139 | + return |
| 140 | + gvas_file = sav_file.replace('.sav', '.sav.gvas') |
| 141 | + with open(gvas_file, 'wb') as f: |
| 142 | + f.write(bytes(uncompressed_data)) |
| 143 | + |
| 144 | +if __name__ == "__main__": |
| 145 | + main() |
| 146 | +``` |
| 147 | + |
| 148 | +Now it correctly decompresses my friend's `Level.sav` into `Level.sav.gvas` where I can follow the rest of [the instructions](https://www.nexusmods.com/palworld/mods/2419) manually. |
| 149 | +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. |
| 150 | + |
| 151 | +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. |
| 152 | + |
| 153 | +## Postface |
| 154 | + |
| 155 | +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: |
| 156 | + |
| 157 | +```text |
| 158 | +lib/linux_arm64/ooz.so |
| 159 | +lib/linux_x86_64/ooz.so |
| 160 | +lib/windows/ooz.pyd |
| 161 | +``` |
| 162 | + |
| 163 | +Of course, how to utilize these files is left as an exercise for the readers. |
0 commit comments