Skip to content

Instantly share code, notes, and snippets.

@cheahjs
Last active February 11, 2024 01:13
Show Gist options
  • Save cheahjs/300239464dd84fe6902893b6b9250fd0 to your computer and use it in GitHub Desktop.
Save cheahjs/300239464dd84fe6902893b6b9250fd0 to your computer and use it in GitHub Desktop.
Converting PalWorld saves to JSON and back

These are Python scripts that are used in conjunction with https://github.com/trumank/uesave-rs to convert PalWorld .sav files into JSON files.

NOTE THAT A LOT OF DATA IS STILL STORED AS BINARY INSIDE OF THE FILES, NOT EVERYTHING IS EASILY EDITABLE IN THE JSON!

Converting Co-op to Dedicated Server saves:

Go over to https://github.com/xNul/palworld-host-save-fix

Windows Instructions

  1. Install Python. You can search for Python in the Microsoft store (any Python 3 version should work)
  2. Click "Download as zip" on the top-right
  3. Extract the zip somewhere.
  4. Download uesave-rs, and extract uesave.exe into the folder you extracted the first zip into.
  5. Drag and drop your save file folder (%LOCALAPPDATA%\Pal\Saved\SaveGames\<SteamID>\<SaveID> onto convert-to-json.bat. This will generate .sav.json files in the folder.
    • This should be the entire folder, not just a single .sav file.
  6. After making any changes, drag and drop the save file folder again onto convert-to-sav.bat to convert the JSON back into the sav files.
@ECHO OFF
@REM Check if python is installed, error if not
python --version 2>NUL
IF %ERRORLEVEL% NEQ 0 (
ECHO Python is not installed. Please install python and try again.
PAUSE
EXIT /B 1
)
@REM Switch to script directory
cd /D "%~dp0"
@REM Check if first argument is a directory
IF NOT EXIST "%~1" (
ECHO You must specify a directory to convert.
PAUSE
EXIT /B 1
)
ECHO This will convert the save files in "%~1" to JSON format.
@REM Ask user if they want to continue
CHOICE /C YN /M "Continue?"
IF %ERRORLEVEL% NEQ 1 (
EXIT /B 1
)
python convert-to-json.py "uesave.exe" "%~1"
PAUSE
@REM Ask user if they want to open the output directory
CHOICE /C YN /M "Open output directory?"
IF %ERRORLEVEL% EQU 1 (
START "" "%~1"
)
#!/usr/bin/env python3
import subprocess
import sys
import glob
import zlib
UESAVE_TYPE_MAPS = [
".worldSaveData.CharacterSaveParameterMap.Key=Struct",
".worldSaveData.FoliageGridSaveDataMap.Key=Struct",
".worldSaveData.FoliageGridSaveDataMap.ModelMap.InstanceDataMap.Key=Struct",
".worldSaveData.MapObjectSpawnerInStageSaveData.Key=Struct",
".worldSaveData.ItemContainerSaveData.Key=Struct",
".worldSaveData.CharacterContainerSaveData.Key=Struct",
]
def main():
# Check if argument exists
if len(sys.argv) < 3:
print('convert-to-json.py <uesave.exe> <save_path>')
exit(1)
# Take the first argument as the path to uesave.exe
uesave_path = sys.argv[1]
# Take the second argument as a path to the save directory
save_path = sys.argv[2]
print(f"Converting .sav files in {save_path} to JSON (using {uesave_path})")
# Find all .sav files in the directory, ignore backup files
files = glob.glob(save_path + '/*.sav') + glob.glob(save_path + '/Players/*.sav')
# Loop through all files
for file in files:
# Open the file
with open(file, 'rb') as f:
# Read the file
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]
save_type = data[11]
# Check for magic bytes
if magic_bytes != b'PlZ':
print(f'File {file} is not a save file, found {magic_bytes} instead of P1Z')
continue
# Valid save types
if save_type not in [0x30, 0x31, 0x32]:
print(f'File {file} has an unknown save type: {save_type}')
continue
# We only have 0x31 (single zlib) and 0x32 (double zlib) saves
if save_type not in [0x31, 0x32]:
print(f'File {file} uses an unhandled compression type: {save_type}')
continue
if save_type == 0x31:
# Check if the compressed length is correct
if compressed_len != len(data) - 12:
print(f'File {file} has an incorrect compressed length: {compressed_len}')
continue
# Decompress file
uncompressed_data = zlib.decompress(data[12:])
if save_type == 0x32:
# Check if the compressed length is correct
if compressed_len != len(uncompressed_data):
print(f'File {file} has an incorrect compressed length: {compressed_len}')
continue
# Decompress file
uncompressed_data = zlib.decompress(uncompressed_data)
# Check if the uncompressed length is correct
if uncompressed_len != len(uncompressed_data):
print(f'File {file} has an incorrect uncompressed length: {uncompressed_len}')
continue
print(f'File {file} uncompressed successfully')
# Convert to json with uesave
# Run uesave.exe with the uncompressed file piped as stdin
# Standard out will be the json string
uesave_run = subprocess.run(uesave_params(uesave_path, file+'.json'), input=uncompressed_data, capture_output=True)
# Check if the command was successful
if uesave_run.returncode != 0:
print(f'uesave.exe failed to convert {file} (return {uesave_run.returncode})')
print(uesave_run.stdout.decode('utf-8'))
print(uesave_run.stderr.decode('utf-8'))
continue
print(f'File {file} (type: {save_type}) converted to JSON successfully')
def uesave_params(uesave_path, out_path):
args = [
uesave_path,
'to-json',
'--output', out_path,
]
for map_type in UESAVE_TYPE_MAPS:
args.append('--type')
args.append(f'{map_type}')
return args
if __name__ == "__main__":
main()
@ECHO OFF
@REM Check if python is installed, error if not
python --version 2>NUL
IF %ERRORLEVEL% NEQ 0 (
ECHO Python is not installed. Please install python and try again.
PAUSE
EXIT /B 1
)
@REM Switch to script directory
cd /D "%~dp0"
@REM Check if first argument is a directory
IF NOT EXIST "%~1" (
ECHO You must specify a directory to convert.
PAUSE
EXIT /B 1
)
ECHO This will convert the save files in JSON format in "%~1" back to .sav format.
ECHO This will overwrite your existing .sav files!
@REM Ask user if they want to continue
CHOICE /C YN /M "Continue?"
IF %ERRORLEVEL% NEQ 1 (
EXIT /B 1
)
python convert-to-sav.py "uesave.exe" "%~1"
PAUSE
@REM Ask user if they want to open the output directory
CHOICE /C YN /M "Open output directory?"
IF %ERRORLEVEL% EQU 1 (
START "" "%~1"
)
#!/usr/bin/env python3
import os
import subprocess
import sys
import glob
import zlib
def main():
# Check if argument exists
if len(sys.argv) < 3:
print('convert-to-sav.py <uesave.exe> <save_path>')
exit(1)
# Take the first argument as the path to uesave.exe
uesave_path = sys.argv[1]
# Take the second argument as a path to the save directory
save_path = sys.argv[2]
print(f"Converting .sav.json files in {save_path} back to .sav (using {uesave_path})")
# Find all .sav.json files in the directory, ignore backup files
files = glob.glob(save_path + '/*.sav.json') + glob.glob(save_path + '/Players/*.sav.json')
# Loop through all files
for file in files:
# Convert the file back to binary
gvas_file = file.replace('.sav.json', '.sav.gvas')
sav_file = file.replace('.sav.json', '.sav')
uesave_run = subprocess.run(uesave_params(uesave_path, file, gvas_file))
if uesave_run.returncode != 0:
print(f'uesave.exe failed to convert {file} (return {uesave_run.returncode})')
continue
# Open the old sav file to get type
if os.path.exists(sav_file):
with open(sav_file, 'rb') as f:
data = f.read()
save_type = data[11]
# If the sav file doesn't exist, use known heuristics
else:
# Largest files use double compression
if sav_file.endswith('LocalData.sav') or sav_file.endswith('Level.sav'):
save_type = 0x32
else:
save_type = 0x31
# Open the binary file
with open(gvas_file, 'rb') as f:
# Read the file
data = f.read()
uncompressed_len = len(data)
compressed_data = zlib.compress(data)
compressed_len = len(compressed_data)
if save_type == 0x32:
compressed_data = zlib.compress(compressed_data)
with open(sav_file, 'wb') as f:
f.write(uncompressed_len.to_bytes(4, byteorder='little'))
f.write(compressed_len.to_bytes(4, byteorder='little'))
f.write(b'PlZ')
f.write(bytes([save_type]))
f.write(bytes(compressed_data))
print(f'Converted {file} to {sav_file}')
def uesave_params(uesave_path, input_file, output_file):
args = [
uesave_path,
'from-json',
'--input', input_file,
'--output', output_file,
]
return args
if __name__ == "__main__":
main()
@FennyFatal
Copy link

Was able to use this to move a save between platforms (From epic to steam).
It's a pain in the butt. Each user save file on a dedicated server has the a guid and an instance id. You have to match up the guid and instance id in the save file or the player won't load, or will load without their guild/exploration data.

Essentially you need to swap the ids between the player files and swap the locations in the world save.

@Jernee
Copy link

Jernee commented Jan 22, 2024

When I drop the save files from my server onto the script it runs fine but nothing actually changes in the file. They are all just .sav still.

@FYWinds
Copy link

FYWinds commented Jan 22, 2024

Got an error when trying to convert from sav to json.

Converting .sav files in ...\4790F9AF9E344A9987A3DE28B3762C72 to JSON (using uesave.exe)
Traceback (most recent call last):
File "...\convert-to-json.py", line 94, in
main()
File "...\convert-to-json.py", line 57, in main
uncompressed_data = zlib.decompress(data[12:])
^^^^^^^^^^^^^^^^^^^^^^^^^^
zlib.error: Error -3 while decompressing data: invalid stored block lengths

Env info

  • Python: 3.11.4
  • Game Ver: Steam Dedicated Server v0.1.2.0
  • Server: Ubuntu 22.04
  • Script running machine: Windows 11

@robotec007
Copy link

Was able to use this to move a save between platforms (From epic to steam). It's a pain in the butt. Each user save file on a dedicated server has the a guid and an instance id. You have to match up the guid and instance id in the save file or the player won't load, or will load without their guild/exploration data.

Essentially you need to swap the ids between the player files and swap the locations in the world save.

Where exactly do i find the id's, i have that excact problem that the level is gone.

@nestharus
Copy link

I get this error

C:\Users\xteam\Downloads\uesave-x86_64-pc-windows-msvc>uesave.exe to-json -i 0DD39D03000000000000000000000000.sav
Found non-standard magic: [85, 14, 00, 00] (�¶ ) expected: GVAS, continuing to parse...
Error: at offset 2571: io error: failed to fill whole buffer

I can't convert any of the .sav files to json. They all have the same error.

Running on Windows 11.

@SC7639
Copy link

SC7639 commented Jan 22, 2024

Was able to use this to move a save between platforms (From epic to steam). It's a pain in the butt. Each user save file on a dedicated server has the a guid and an instance id. You have to match up the guid and instance id in the save file or the player won't load, or will load without their guild/exploration data.

Essentially you need to swap the ids between the player files and swap the locations in the world save.

I'm trying to do this between Games pass and Steam

@avasiaxx
Copy link

Followed all directions. Nothing happens. Files stay the same.

@juckthaltnicht
Copy link

python: can't open file 'C:\Users\CENSORED\Desktop\SaveConverter\convert-to-json.py': [Errno 2] No such file or directory

@CelicaXX
Copy link

Works perfect. However, the Level.sav file is 1GB/250,000,000 lines+ after being uncompressed. Would be nice if someone smarter than me was able to take this data and import it to something like an SQL database. VSCodium does not like working with that many lines.

@CelicaXX
Copy link

I get this error

C:\Users\xteam\Downloads\uesave-x86_64-pc-windows-msvc>uesave.exe to-json -i 0DD39D03000000000000000000000000.sav Found non-standard magic: [85, 14, 00, 00] (�¶ ) expected: GVAS, continuing to parse... Error: at offset 2571: io error: failed to fill whole buffer

I can't convert any of the .sav files to json. They all have the same error.

Running on Windows 11.

For all of you guys having issues It's probably because the batch file is expecting you to drag a Folder/Directory onto it as the input. Not single.sav files.

@CelicaXX
Copy link

python: can't open file 'C:\Users\CENSORED\Desktop\SaveConverter\convert-to-json.py': [Errno 2] No such file or directory

Did you download all 5 files? convert-to-json.bat, convert-to-json.py, convert-to-sav.bat, convert-to-sav.py
Do you have uesave.exe in the same directory as the 5 files?

@mauzao9
Copy link

mauzao9 commented Jan 23, 2024

Am trying to figure out how could I deal with opening a save that's almost 10GB now, notepad++, even visual studio are completely F, searching for the specific ID I'm looking for... even worse.

@FennyFatal
Copy link

Am trying to figure out how could I deal with opening a save that's almost 10GB now, notepad++, even visual studio are completely F, searching for the specific ID I'm looking for... even worse.

I suggest using sed to do an inline replace.

@Romafrique
Copy link

I have some trouble from the start: Dropping the file on the bat program and its teling me python not instaled...

BTW i got the latest release 3.12.1

@DeathWithKebab
Copy link

I have some trouble from the start: Dropping the file on the bat program and its teling me python not instaled...

BTW i got the latest release 3.12.1

Modifying "python" into "python3" or "py" should help... Worked for me as it may not recognize the command on your system

@Hdom
Copy link

Hdom commented Jan 24, 2024

Latest update broke ability to convert WorldOption .json to .sav

Loading JSON from C:\temp\WorldOption.sav.json
Encoding group data
Traceback (most recent call last):
File "D:\uesave\convert-single-json-to-sav.py", line 215, in
main()
File "D:\uesave\convert-single-json-to-sav.py", line 201, in main
encode_group_data(data)
File "D:\uesave\convert-single-json-to-sav.py", line 148, in encode_group_data
group_map = level_json['root']['properties']['worldSaveData']['Struct']['value']['Struct']['GroupSaveDataMap']['Map']['value']
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^
KeyError: 'worldSaveData'

Here is link to previous version https://gist.github.com/cheahjs/300239464dd84fe6902893b6b9250fd0/b2dfb811a281ff33d9edc5cd8253716c794888b2
I downloaded zip and it works fine.

@cheahjs
Copy link
Author

cheahjs commented Jan 24, 2024

Latest update broke ability to convert WorldOption .json to .sav

Loading JSON from C:\temp\WorldOption.sav.json
Encoding group data
Traceback (most recent call last):
File "D:\uesave\convert-single-json-to-sav.py", line 215, in
main()
File "D:\uesave\convert-single-json-to-sav.py", line 201, in main
encode_group_data(data)
File "D:\uesave\convert-single-json-to-sav.py", line 148, in encode_group_data
group_map = level_json['root']['properties']['worldSaveData']['Struct']['value']['Struct']['GroupSaveDataMap']['Map']['value']

KeyError: 'worldSaveData'

Here is link to previous version https://gist.github.com/cheahjs/300239464dd84fe6902893b6b9250fd0/b2dfb811a281ff33d9edc5cd8253716c794888b2 I downloaded zip and it works fine.

Sorry about that, it should be fixed now

@DKingAlpha
Copy link

Ox30 is uncompressed. Its actually supported by the game.

@Busterblader7
Copy link

I need a video guide 😵

@Romafrique
Copy link

I have some trouble from the start: Dropping the file on the bat program and its teling me python not instaled...
BTW i got the latest release 3.12.1

Modifying "python" into "python3" or "py" should help... Worked for me as it may not recognize the command on your system

I'll try as soon as I get off work, thanks !

@Kakaka999
Copy link

error 13 permission denied still happening to me for convert sav to json

@Kakaka999
Copy link

I've downloaded a older version and tried it again and got this error
Python 3.12.1
This will convert the save files in "D:\SteamLibrary\steamapps\common\PalServer\Pal\Saved\SaveGames\0\0150E669444CD2C49BCC7E94832F116C" to JSON format.
Continue? [Y,N]?Y
Converting .sav files in D:\SteamLibrary\steamapps\common\PalServer\Pal\Saved\SaveGames\0\0150E669444CD2C49BCC7E94832F116C to JSON (using uesave.exe)
File D:\SteamLibrary\steamapps\common\PalServer\Pal\Saved\SaveGames\0\0150E669444CD2C49BCC7E94832F116C\Level.sav uncompressed successfully
Traceback (most recent call last):
File "C:\Users\AdminK\Downloads\300239464dd84fe6902893b6b9250fd0-b2dfb811a281ff33d9edc5cd8253716c794888b2\300239464dd84fe6902893b6b9250fd0-b2dfb811a281ff33d9edc5cd8253716c794888b2\convert-to-json.py", line 94, in
main()
File "C:\Users\AdminK\Downloads\300239464dd84fe6902893b6b9250fd0-b2dfb811a281ff33d9edc5cd8253716c794888b2\300239464dd84fe6902893b6b9250fd0-b2dfb811a281ff33d9edc5cd8253716c794888b2\convert-to-json.py", line 73, in main
uesave_run = subprocess.run(uesave_params(uesave_path, file+'.json'), input=uncompressed_data, capture_output=True)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.496.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 548, in run
with Popen(*popenargs, **kwargs) as process:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.496.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1026, in init
self._execute_child(args, executable, preexec_fn, close_fds,
File "C:\Program Files\WindowsApps\PythonSoftwareFoundation.Python.3.12_3.12.496.0_x64__qbz5n2kfra8p0\Lib\subprocess.py", line 1538, in _execute_child
hp, ht, pid, tid = _winapi.CreateProcess(executable, args,
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
FileNotFoundError: [WinError 2] 지정된 파일을 찾을 수 없습니다

@iebb
Copy link

iebb commented Jan 24, 2024

Created a website for this - https://palworld.tf/ , where you can do this conversion in-browser by dragging files with wasm-compiled uesave-rs without downloading these.

@KrisDevel0pment
Copy link

Created a website for this - https://palworld.tf/ , where you can do this conversion in-browser by dragging files with wasm-compiled uesave-rs without downloading these.

Doesn't work.

@iebb
Copy link

iebb commented Jan 25, 2024

Created a website for this - https://palworld.tf/ , where you can do this conversion in-browser by dragging files with wasm-compiled uesave-rs without downloading these.

Doesn't work.

wait a moment, just messed up with the latest commit... should work now

@bhaktas
Copy link

bhaktas commented Jan 26, 2024

is there a way to convert steam save to xgp save?

@DKingAlpha
Copy link

Script to find PlayerUId (Players/<GUID>.sav)

#!/usr/bin/python3

from pathlib import Path
from struct import unpack_from
import json

j = json.loads(Path('temp/Level.2.json').read_text())
CharacterSaveParameterMap = j['root']['properties']['worldSaveData']['Struct']['value']['Struct']['CharacterSaveParameterMap']['Map']['value']

def find_user(name: str) -> str:
    for item in CharacterSaveParameterMap:
        key = item['key']
        value = item['value']
        PlayerUId = key['Struct']['Struct']['PlayerUId']['Struct']['value']['Guid']
        PlayerUIdInt = int(PlayerUId.replace('-', ''), 16)
        if PlayerUIdInt == 0:
            continue
        RawDataBytes = bytes(value['Struct']['Struct']['RawData']['Array']['Base']['Byte']['Byte'])
        pattern = b'\x4E\x69\x63\x6B\x4E\x61\x6D\x65\x00\x0C\x00\x00\x00\x53\x74\x72\x50\x72\x6F\x70\x65\x72\x74\x79\x00'
        offset = RawDataBytes.find(pattern)
        off_size = 0x22
        if offset == -1:
            print('not found')
            continue
        NickNameSize = unpack_from('<i', RawDataBytes, offset+off_size)[0]
        if NickNameSize >= 0:
            NickName = RawDataBytes[offset+off_size+4:offset+off_size+4+NickNameSize].decode('utf-8')
        else:
            max_bytes = (-NickNameSize) * 4
            NickName = RawDataBytes[offset+off_size+4:offset+off_size+4+max_bytes].decode('utf-16-le')[:-NickNameSize]
        NickName = NickName.rstrip('\x00')
        print(PlayerUId, NickName, end='')
        if NickName == name:
            print('\t\t\tFOUND')
        else:
            print('')

find_user('DKingAlpha')

@DKingAlpha
Copy link

@ShadyGame
Copy link

Got an error when trying to convert from sav to json.

Converting .sav files in ...\4790F9AF9E344A9987A3DE28B3762C72 to JSON (using uesave.exe)
Traceback (most recent call last):
File "...\convert-to-json.py", line 94, in
main()
File "...\convert-to-json.py", line 57, in main
uncompressed_data = zlib.decompress(data[12:])
^^^^^^^^^^^^^^^^^^^^^^^^^^
zlib.error: Error -3 while decompressing data: invalid stored block lengths

Env info

  • Python: 3.11.4
  • Game Ver: Steam Dedicated Server v0.1.2.0
  • Server: Ubuntu 22.04
  • Script running machine: Windows 11

i have the same error any solution ?

@Croock92
Copy link

Croock92 commented Feb 7, 2024

Got a keyerror: 'groupsavedatamap' someone can help me ? :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment