Skip to content

Commit ead33b7

Browse files
committed
New article: /p/77
1 parent 81d0cbb commit ead33b7

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
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

Comments
 (0)