Created
June 29, 2021 17:20
-
-
Save monodop/f74ee058c173d526713317e1aa2870aa to your computer and use it in GitHub Desktop.
Convert Snowrunner UWP Saves to Steam
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import os | |
import struct | |
import sys | |
import uuid | |
import zipfile | |
import json | |
# Snowrunner save "converter" (Xbox Game Pass for PC -> Steam) | |
# Might work for other games available with the Game Pass if the app name is changed. | |
# Running: Just run the script with Python 3 to create a ZIP file that contains the save files | |
# Thanks to @Z1ni for building the original Yakuza save converted that this was modified from: https://gist.github.com/Z1ni/7a8c1927343dd5570fff0d015f425cca | |
# Thanks to @snoozbuster for figuring out the container format at https://github.com/goatfungus/NMSSaveEditor/issues/306 | |
xgp_app = "FocusHomeInteractiveSA.SnowRunnerWindows10_4hny5m903y3g0" | |
save_zip_path = "saves.zip" | |
def read_utf16_str(f): | |
str_len = struct.unpack("<i", f.read(4))[0] | |
return f.read(str_len * 2).decode("utf-16") | |
def read_utf16_strc(f, c): | |
return f.read(c).decode("utf-16") | |
def get_file_paths(): | |
# Find gamepass save dir | |
wgs_dir = os.path.expandvars(f"%LOCALAPPDATA%\\Packages\\{xgp_app}\\SystemAppData\\wgs") | |
# Get the correct user directory | |
dirs = [d for d in os.listdir(wgs_dir) if d != "t"] | |
dir_count = len(dirs) | |
if dir_count != 1: | |
raise Exception(f"Expected one user directory in wgs directory, found {dir_count}") | |
containers_dir = os.path.join(wgs_dir, dirs[0]) | |
containers_idx_path = os.path.join(containers_dir, "containers.index") | |
save_files = [] | |
# Read the index file | |
with open(containers_idx_path, "rb") as f: | |
# Unknown | |
f.read(4) | |
file_count = struct.unpack("<i", f.read(4))[0] | |
print(file_count) | |
# Unknown | |
f.read(4) | |
store_pkg_name = read_utf16_str(f) | |
print(store_pkg_name) | |
# Unknown | |
f.read(12) | |
read_utf16_str(f) | |
# Unknown | |
f.read(8) | |
for _ in range(file_count): | |
# File name | |
fname = read_utf16_str(f) | |
# Duplicate of the file name | |
read_utf16_str(f) | |
# Unknown quoted hex number | |
read_utf16_str(f) | |
# Container number | |
container_num = struct.unpack("B", f.read(1))[0] | |
# Unknown | |
f.read(4) | |
# Read container (folder) GUID | |
container_guid = uuid.UUID(bytes_le=f.read(16)) | |
# Unknown | |
f.read(24) | |
print(fname, container_num, container_guid) | |
# Read the container file in the file directory | |
with open(os.path.join(containers_dir, container_guid.hex.upper(), f"container.{container_num}"), "rb") as cf: | |
# Unknown | |
cf.read(8) | |
while cf.read(1): | |
cf.seek(-1, 1) | |
# Output filename (steam format) | |
output_filename = read_utf16_strc(cf, 128).strip('\0') + '.cfg' | |
# File guid (uwp format) | |
file_guid = uuid.UUID(bytes_le=cf.read(16)) | |
print(" - ", output_filename, file_guid) | |
# Unknown | |
cf.read(16) | |
save_files.append((output_filename, os.path.join(containers_dir, container_guid.hex.upper(), file_guid.hex.upper()))) | |
return save_files | |
def main(): | |
# Get save file paths with correct names | |
save_files = get_file_paths() | |
# Store the save files into a zip | |
try: | |
with zipfile.ZipFile(save_zip_path, "x") as save_zip: | |
for info in save_files: | |
orig_fname, container_fname = info | |
save_zip.write(container_fname, arcname=orig_fname) | |
except FileExistsError: | |
print(f"Save file container ZIP \"{save_zip_path}\" already exists.") | |
sys.exit(1) | |
print(f"Save files stored to \"{save_zip_path}\"") | |
sys.exit(0) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment