Merge pull request #8 from otavepto/patch/cloud-dirs

Support parsing cloud dirs
This commit is contained in:
Detanup01
2025-10-29 23:52:34 +01:00
committed by GitHub
2 changed files with 208 additions and 1 deletions

View File

@@ -0,0 +1,177 @@
class SaveFileModel:
def __init__(self, root: str, path_after_root: str, platforms: set[str]):
self.root = root
self.path_after_root = path_after_root
self.platforms = platforms or set()
class SaveFileOverrideModel:
def __init__(self,
root_original: str, root_new: str, path_after_root_new: str,
platform: str, paths_to_transform: list[tuple[str, str]]
):
self.root_original = root_original
self.root_new = root_new
self.path_after_root_new = path_after_root_new
self.platform = platform
self.paths_to_transform = paths_to_transform or []
class Ufs:
def __init__(self, save_files: list[SaveFileModel] = None, save_file_overrides: list[SaveFileOverrideModel] = None):
self.save_files = save_files or []
self.save_file_overrides = save_file_overrides or []
def parse_cloud_dirs(game_info: dict[str, object]) -> tuple[list[SaveFileModel], list[SaveFileOverrideModel]]:
save_files_raw: list[dict] = game_info.get("ufs", {}).get("savefiles", {}).values()
if not save_files_raw:
return ([], [])
save_files: list[SaveFileModel] = []
for item in save_files_raw:
root: str = item.get("root", "")
if not root:
continue
path: str = item.get("path", "")
platforms: set[str] = set(item.get("platforms", {}).values())
save_files.append(SaveFileModel(
root=root, path_after_root=path, platforms=platforms
))
root_overrides_raw: list[dict] = game_info.get("ufs", {}).get("rootoverrides", {}).values()
root_overrides: list[SaveFileOverrideModel] = []
for item in root_overrides_raw:
root_original = item.get("root", "")
root_new = item.get("useinstead", "")
platform = item.get("os", "")
if not root_original:
print("[?] UFS override has empty root original/new, or empty platform")
continue
os_compare = item.get("oscompare", "")
if os_compare != "=":
print(f"[?] UFS override for {root_original}@{platform} >> {root_new} has unknown OS comparison operation '{os_compare}'")
path_after_root_new = item.get("addpath", "")
paths_to_transform: list[tuple[str, str]] = list(map(
lambda obj: (obj.get("find", ""), obj.get("replace", "")),
item.get("pathtransforms", {}).values()
))
root_overrides.append(SaveFileOverrideModel(
root_original=root_original, root_new=root_new, path_after_root_new=path_after_root_new,
platform=platform, paths_to_transform=paths_to_transform
))
return (save_files, root_overrides)
def get_ufs_dirs(
platform: str,
save_files: list[SaveFileModel],
save_file_overrides: list[SaveFileOverrideModel]
) -> list[str]:
def sanitize_path(path: str) -> str:
# appid 292930 sets "path=/"
path = path.strip("/")
# appid 282800 sets "path=save/{64BitSteamID}/."
while path.endswith("/."):
path = path[:-2]
while path.startswith("./"):
path = path[2:]
# remove any "/." in between
while True:
fidx = path.find("/./")
if fidx < 0:
break
path = path[:fidx] + path[fidx + 2:]
if "." == path:
return ""
return path
def fixup_vars(path: str) -> str:
return path.replace(
"{64BitSteamID}", "{::64BitSteamID::}"
).replace(
"{Steam3AccountID}", "{::Steam3AccountID::}"
)
if not save_files:
return []
# add base save files
ufs = Ufs()
for item in save_files:
if not item.platforms: # all platforms
ufs.save_files.append(item)
elif any(platfrom.upper() == "ALL" for platfrom in item.platforms):
# appid 130 and appid 50 use "all"
ufs.save_files.append(item)
elif any(platfrom.upper() == platform.upper() for platfrom in item.platforms):
ufs.save_files.append(item)
# add overrides
for item in save_file_overrides:
if item.platform.upper() == platform.upper():
ufs.save_file_overrides.append(item)
# format the root identifiers like this:
# {SteamCloudDocuments} >> {::SteamCloudDocuments::}
# this char ':' is illegal on all OSes and fails to create a dir
# if any idetifier was not substituted
# some games like appid 388880 have broken config, the emu can
# then easily detect that by looking for the pattern "::" or "{::"
# and decide the appropriate action to take
paths: set[str] = set()
# if we have overrides then only use them
if ufs.save_file_overrides:
for ufs_override in ufs.save_file_overrides:
new_path = f"{{::{ufs_override.root_new.strip()}::}}"
path_after_root_new = sanitize_path(ufs_override.path_after_root_new.replace("\\", "/"))
if path_after_root_new:
new_path += f"/{path_after_root_new}"
save_files_to_override: list[SaveFileModel] = list(filter(
lambda save: save.root.upper() == ufs_override.root_original.upper(),
ufs.save_files
))
for save_file in save_files_to_override:
# don't sanitize "save_file.path_after_root" yet, we need to find and replace substrings
path_after_root_original = save_file.path_after_root.replace("\\", "/")
for (find, replace) in ufs_override.paths_to_transform:
find = find.replace("\\", "/")
replace = replace.replace("\\", "/")
if find and path_after_root_original:
path_after_root_original = path_after_root_original.replace(find, replace)
elif not find and not path_after_root_original:
# when "override.find" and "root.path" are both empty
# it is expected to use the replace string directly
# example: appid 2174720
path_after_root_original = replace
else:
print(
f"UFS override for {save_file.root}@{ufs_override.platform} >> {ufs_override.root_new} has empty 'find' string, " +
f"or original UFS has empty 'path' string, ignoring"
)
path_after_root_original = sanitize_path(path_after_root_original)
if path_after_root_original:
new_path += f"/{path_after_root_original}"
paths.add(fixup_vars(new_path))
else: # otherwise (no overrides) use all relevant UFS entries
for save_file in ufs.save_files:
new_path = f"{{::{save_file.root.strip()}::}}"
path_after_root = sanitize_path(save_file.path_after_root.replace("\\", "/"))
if path_after_root:
new_path += f"/{path_after_root}"
paths.add(fixup_vars(new_path))
return list(paths)

View File

@@ -2,7 +2,8 @@ import pathlib
import time
from stats_schema_achievement_gen import achievements_gen
from external_components import (
ach_watcher_gen, cdx_gen, app_images, app_details, safe_name
ach_watcher_gen, cdx_gen, app_images, app_details, safe_name,
cloud_dirs
)
from controller_config_generator import parse_controller_vdf
from steam.client import SteamClient
@@ -581,6 +582,7 @@ def help():
print(" -skip_ach: skip downloading & generating achievements and their images")
print(" -skip_con: skip downloading & generating controller configuration files")
print(" -skip_inv: skip downloading & generating inventory data (items.json & default_items.json)")
print(" -skip_cloud_dirs: skip parsing directories for cloud saves")
print("\nAll switches are optional except app id, at least 1 app id must be provided")
print("\nAutomate the login prompt:")
print(" * You can create a file called 'my_login.txt' beside the script, then add your username on the first line")
@@ -631,6 +633,7 @@ def main():
SKIP_ACH = False
SKIP_CONTROLLER = False
SKIP_INVENTORY = False
SKIP_CLOUD_DIRS = False
prompt_for_unavailable = True
@@ -674,6 +677,8 @@ def main():
SKIP_CONTROLLER = True
elif f'{appid}'.lower() == '-skip_inv':
SKIP_INVENTORY = True
elif f'{appid}'.lower() == '-skip_cloud_dirs':
SKIP_CLOUD_DIRS = True
else:
print(f'[X] invalid switch: {appid}')
help()
@@ -1027,6 +1032,31 @@ def main():
logo,
logo_small)
if not SKIP_CLOUD_DIRS:
(save_files, save_file_overrides) = cloud_dirs.parse_cloud_dirs(game_info)
win_cloud_dirs = cloud_dirs.get_ufs_dirs("Windows", save_files, save_file_overrides)
for idx in range(len(win_cloud_dirs)):
merge_dict(out_config_app_ini, {
'configs.app.ini': {
'app::cloud_save::win': {
f"dir{idx + 1}": (win_cloud_dirs[idx], ''),
}
}
})
linux_cloud_dirs = cloud_dirs.get_ufs_dirs("Linux", save_files, save_file_overrides)
for idx in range(len(linux_cloud_dirs)):
merge_dict(out_config_app_ini, {
'configs.app.ini': {
'app::cloud_save::linux': {
f"dir{idx + 1}": (linux_cloud_dirs[idx], ''),
}
}
})
if DISABLE_EXTRA:
merge_dict(out_config_app_ini, EXTRA_FEATURES_DISABLE)