24 globs = [
"*.sl2", os.path.join(
"*",
"*.sl2")]
25 home = os.path.expanduser(
"~")
26 appdata = os.environ.get(
"APPDATA")
28 globs.append(os.path.join(appdata,
"*",
"*.sl2"))
29 globs.append(os.path.join(appdata,
"*",
"*",
"*.sl2"))
33 "drive_c/users/steamuser/AppData/Roaming/*/*.sl2",
34 "drive_c/users/steamuser/AppData/Roaming/*/*/*.sl2",
37 "drive_c/users/*/AppData/Roaming/*/*.sl2",
38 "drive_c/users/*/AppData/Roaming/*/*/*.sl2",
41 for steam
in (
".local/share/Steam",
".steam/steam",
".steam/root"):
43 globs.append(os.path.join(home, steam,
"steamapps/compatdata/*/pfx", tail))
47 "Games/Heroic/Prefixes/default/*/pfx",
48 "Games/Heroic/Prefixes/*/pfx",
49 ".config/heroic/prefixes/default/*/pfx",
53 globs.append(os.path.join(home, heroic, tail))
55 for tail
in user_roaming:
56 globs.append(os.path.join(home,
".local/share/lutris/*/pfx", tail))
57 globs.append(os.path.join(home,
".wine", tail))
91 ap = argparse.ArgumentParser(
92 description=
"FromSoftware .sl2 save -> Markdown or JSON playthrough summary "
93 "(DS PtDE/Remastered, DS2 vanilla/SOTFS, DS3, Sekiro, Elden Ring)",
94 epilog=
"Metadata example: --meta source=Steam --meta os='Nobara 43' "
95 "--meta launcher=Heroic --meta proton='GE-Proton 9-20' "
96 "--meta dlc='Ashes of Ariandel' --meta dlc='The Ringed City'. "
97 "A key repeated becomes a list. Any key is accepted.",
102 help=
"path to a .sl2 save, or a FOLDER of them (auto-detected if "
103 "omitted). Several paths, or one folder, produce a combined "
104 "playthrough document covering every character found.",
109 default=
"playthrough.md",
110 help=
"output path; a .json extension selects JSON",
114 choices=(
"auto",
"md",
"json"),
116 help=
"output format (default: from the -o extension)",
122 help=
"record how the game was run: store, version, DLC, OS, "
123 "launcher, Proton build, anything. Repeatable; a repeated "
124 "key becomes a list. None of it is read from the save.",
129 help=
"JSON object of the same metadata, merged underneath --meta",
135 help=
"JSON indent; 0 for one dense line (default: 2)",
140 help=
"run the validation pass and add its findings to the output: "
141 "states an unmodified game could not produce, and internal "
142 "contradictions. Off by default, and it never changes what is parsed",
147 help=
"force the combined document even for a single save",
149 args = ap.parse_args()
152 meta = parse_meta(args.meta, args.meta_json)
153 except (OSError, ValueError)
as exc:
157 base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
162 paths = list(args.sl2)
166 files += find_saves(p)
167 elif os.path.isfile(p):
168 files.append(os.path.abspath(p))
170 sys.exit(f
"No such file or folder: {p}")
171 if paths
and not files:
172 sys.exit(
"No .sl2 files found in: " +
", ".join(paths))
174 if args.combined
or len(files) > 1
or any(os.path.isdir(p)
for p
in paths):
175 text = build_combined(files, base_dir, meta)
177 sys.exit(
"None of those files could be read as a supported save.")
182 if not os.path.isfile(sl2):
183 sys.exit(f
"No such file: {sl2}")
184 with open(sl2,
"rb")
as f:
189 fmt =
"json" if args.out.lower().endswith(
".json")
else "md"
191 name = os.path.basename(sl2)
192 save = parse_save(data, base_dir)
196 build_json(save, name, meta, args.validate, data),
198 indent=args.indent
or None,
202 text = render_markdown(save, name, meta, args.validate, data)
217 start = save.cfg[
"slots"].start
218 for i, ch
in save.characters:
219 label = f
"Slot {i - start + 1}" + (f
": {ch['name']}" if ch.get(
"name")
else "")
221 print(
"\n".join(validation_text(run_validation(ch, save.game), label)))
222 entries = parse_bnd4(data)
or []
223 report = run_file_validation(save.game, entries, data, checksum_ok)
225 print(
"\n".join(validation_text_file(report)))