Skip to content

Commit 8cd743e

Browse files
authored
Merge pull request Detanup01#8 from otavepto/patch/cloud-dirs
Support parsing cloud dirs
2 parents e87754c + 764c7c7 commit 8cd743e

File tree

2 files changed

+208
-1
lines changed

2 files changed

+208
-1
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
2+
class SaveFileModel:
3+
def __init__(self, root: str, path_after_root: str, platforms: set[str]):
4+
self.root = root
5+
self.path_after_root = path_after_root
6+
self.platforms = platforms or set()
7+
8+
9+
class SaveFileOverrideModel:
10+
def __init__(self,
11+
root_original: str, root_new: str, path_after_root_new: str,
12+
platform: str, paths_to_transform: list[tuple[str, str]]
13+
):
14+
self.root_original = root_original
15+
self.root_new = root_new
16+
self.path_after_root_new = path_after_root_new
17+
self.platform = platform
18+
self.paths_to_transform = paths_to_transform or []
19+
20+
class Ufs:
21+
def __init__(self, save_files: list[SaveFileModel] = None, save_file_overrides: list[SaveFileOverrideModel] = None):
22+
self.save_files = save_files or []
23+
self.save_file_overrides = save_file_overrides or []
24+
25+
26+
def parse_cloud_dirs(game_info: dict[str, object]) -> tuple[list[SaveFileModel], list[SaveFileOverrideModel]]:
27+
save_files_raw: list[dict] = game_info.get("ufs", {}).get("savefiles", {}).values()
28+
if not save_files_raw:
29+
return ([], [])
30+
31+
save_files: list[SaveFileModel] = []
32+
for item in save_files_raw:
33+
root: str = item.get("root", "")
34+
if not root:
35+
continue
36+
37+
path: str = item.get("path", "")
38+
platforms: set[str] = set(item.get("platforms", {}).values())
39+
save_files.append(SaveFileModel(
40+
root=root, path_after_root=path, platforms=platforms
41+
))
42+
43+
root_overrides_raw: list[dict] = game_info.get("ufs", {}).get("rootoverrides", {}).values()
44+
root_overrides: list[SaveFileOverrideModel] = []
45+
for item in root_overrides_raw:
46+
root_original = item.get("root", "")
47+
root_new = item.get("useinstead", "")
48+
platform = item.get("os", "")
49+
if not root_original:
50+
print("[?] UFS override has empty root original/new, or empty platform")
51+
continue
52+
53+
os_compare = item.get("oscompare", "")
54+
if os_compare != "=":
55+
print(f"[?] UFS override for {root_original}@{platform} >> {root_new} has unknown OS comparison operation '{os_compare}'")
56+
57+
path_after_root_new = item.get("addpath", "")
58+
paths_to_transform: list[tuple[str, str]] = list(map(
59+
lambda obj: (obj.get("find", ""), obj.get("replace", "")),
60+
item.get("pathtransforms", {}).values()
61+
))
62+
root_overrides.append(SaveFileOverrideModel(
63+
root_original=root_original, root_new=root_new, path_after_root_new=path_after_root_new,
64+
platform=platform, paths_to_transform=paths_to_transform
65+
))
66+
67+
return (save_files, root_overrides)
68+
69+
70+
def get_ufs_dirs(
71+
platform: str,
72+
save_files: list[SaveFileModel],
73+
save_file_overrides: list[SaveFileOverrideModel]
74+
) -> list[str]:
75+
def sanitize_path(path: str) -> str:
76+
# appid 292930 sets "path=/"
77+
path = path.strip("/")
78+
# appid 282800 sets "path=save/{64BitSteamID}/."
79+
while path.endswith("/."):
80+
path = path[:-2]
81+
while path.startswith("./"):
82+
path = path[2:]
83+
84+
# remove any "/." in between
85+
while True:
86+
fidx = path.find("/./")
87+
if fidx < 0:
88+
break
89+
path = path[:fidx] + path[fidx + 2:]
90+
91+
if "." == path:
92+
return ""
93+
94+
return path
95+
96+
def fixup_vars(path: str) -> str:
97+
return path.replace(
98+
"{64BitSteamID}", "{::64BitSteamID::}"
99+
).replace(
100+
"{Steam3AccountID}", "{::Steam3AccountID::}"
101+
)
102+
103+
if not save_files:
104+
return []
105+
106+
# add base save files
107+
ufs = Ufs()
108+
for item in save_files:
109+
if not item.platforms: # all platforms
110+
ufs.save_files.append(item)
111+
elif any(platfrom.upper() == "ALL" for platfrom in item.platforms):
112+
# appid 130 and appid 50 use "all"
113+
ufs.save_files.append(item)
114+
elif any(platfrom.upper() == platform.upper() for platfrom in item.platforms):
115+
ufs.save_files.append(item)
116+
117+
# add overrides
118+
for item in save_file_overrides:
119+
if item.platform.upper() == platform.upper():
120+
ufs.save_file_overrides.append(item)
121+
122+
# format the root identifiers like this:
123+
# {SteamCloudDocuments} >> {::SteamCloudDocuments::}
124+
# this char ':' is illegal on all OSes and fails to create a dir
125+
# if any idetifier was not substituted
126+
# some games like appid 388880 have broken config, the emu can
127+
# then easily detect that by looking for the pattern "::" or "{::"
128+
# and decide the appropriate action to take
129+
130+
paths: set[str] = set()
131+
# if we have overrides then only use them
132+
if ufs.save_file_overrides:
133+
for ufs_override in ufs.save_file_overrides:
134+
new_path = f"{{::{ufs_override.root_new.strip()}::}}"
135+
path_after_root_new = sanitize_path(ufs_override.path_after_root_new.replace("\\", "/"))
136+
if path_after_root_new:
137+
new_path += f"/{path_after_root_new}"
138+
139+
save_files_to_override: list[SaveFileModel] = list(filter(
140+
lambda save: save.root.upper() == ufs_override.root_original.upper(),
141+
ufs.save_files
142+
))
143+
for save_file in save_files_to_override:
144+
# don't sanitize "save_file.path_after_root" yet, we need to find and replace substrings
145+
path_after_root_original = save_file.path_after_root.replace("\\", "/")
146+
for (find, replace) in ufs_override.paths_to_transform:
147+
find = find.replace("\\", "/")
148+
replace = replace.replace("\\", "/")
149+
if find and path_after_root_original:
150+
path_after_root_original = path_after_root_original.replace(find, replace)
151+
elif not find and not path_after_root_original:
152+
# when "override.find" and "root.path" are both empty
153+
# it is expected to use the replace string directly
154+
# example: appid 2174720
155+
path_after_root_original = replace
156+
else:
157+
print(
158+
f"UFS override for {save_file.root}@{ufs_override.platform} >> {ufs_override.root_new} has empty 'find' string, " +
159+
f"or original UFS has empty 'path' string, ignoring"
160+
)
161+
162+
path_after_root_original = sanitize_path(path_after_root_original)
163+
if path_after_root_original:
164+
new_path += f"/{path_after_root_original}"
165+
166+
paths.add(fixup_vars(new_path))
167+
else: # otherwise (no overrides) use all relevant UFS entries
168+
for save_file in ufs.save_files:
169+
new_path = f"{{::{save_file.root.strip()}::}}"
170+
path_after_root = sanitize_path(save_file.path_after_root.replace("\\", "/"))
171+
if path_after_root:
172+
new_path += f"/{path_after_root}"
173+
174+
paths.add(fixup_vars(new_path))
175+
176+
177+
return list(paths)

generate_emu_config_old/generate_emu_config.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22
import time
33
from stats_schema_achievement_gen import achievements_gen
44
from external_components import (
5-
ach_watcher_gen, cdx_gen, app_images, app_details, safe_name
5+
ach_watcher_gen, cdx_gen, app_images, app_details, safe_name,
6+
cloud_dirs
67
)
78
from controller_config_generator import parse_controller_vdf
89
from steam.client import SteamClient
@@ -581,6 +582,7 @@ def help():
581582
print(" -skip_ach: skip downloading & generating achievements and their images")
582583
print(" -skip_con: skip downloading & generating controller configuration files")
583584
print(" -skip_inv: skip downloading & generating inventory data (items.json & default_items.json)")
585+
print(" -skip_cloud_dirs: skip parsing directories for cloud saves")
584586
print("\nAll switches are optional except app id, at least 1 app id must be provided")
585587
print("\nAutomate the login prompt:")
586588
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():
631633
SKIP_ACH = False
632634
SKIP_CONTROLLER = False
633635
SKIP_INVENTORY = False
636+
SKIP_CLOUD_DIRS = False
634637

635638
prompt_for_unavailable = True
636639

@@ -674,6 +677,8 @@ def main():
674677
SKIP_CONTROLLER = True
675678
elif f'{appid}'.lower() == '-skip_inv':
676679
SKIP_INVENTORY = True
680+
elif f'{appid}'.lower() == '-skip_cloud_dirs':
681+
SKIP_CLOUD_DIRS = True
677682
else:
678683
print(f'[X] invalid switch: {appid}')
679684
help()
@@ -1027,6 +1032,31 @@ def main():
10271032
logo,
10281033
logo_small)
10291034

1035+
if not SKIP_CLOUD_DIRS:
1036+
(save_files, save_file_overrides) = cloud_dirs.parse_cloud_dirs(game_info)
1037+
1038+
win_cloud_dirs = cloud_dirs.get_ufs_dirs("Windows", save_files, save_file_overrides)
1039+
for idx in range(len(win_cloud_dirs)):
1040+
merge_dict(out_config_app_ini, {
1041+
'configs.app.ini': {
1042+
'app::cloud_save::win': {
1043+
f"dir{idx + 1}": (win_cloud_dirs[idx], ''),
1044+
}
1045+
}
1046+
})
1047+
1048+
1049+
linux_cloud_dirs = cloud_dirs.get_ufs_dirs("Linux", save_files, save_file_overrides)
1050+
for idx in range(len(linux_cloud_dirs)):
1051+
merge_dict(out_config_app_ini, {
1052+
'configs.app.ini': {
1053+
'app::cloud_save::linux': {
1054+
f"dir{idx + 1}": (linux_cloud_dirs[idx], ''),
1055+
}
1056+
}
1057+
})
1058+
1059+
10301060
if DISABLE_EXTRA:
10311061
merge_dict(out_config_app_ini, EXTRA_FEATURES_DISABLE)
10321062

0 commit comments

Comments
 (0)