71 with open(path,
"rb")
as f:
72 save = parse_save(f.read(), base_dir)
73 except (OSError, ValueError, SystemExit):
75 cfg = GAMES[save.game]
76 start = cfg[
"slots"].start
78 snapshot(ch, path, i - start + 1, save.game, cfg[
"title"])
79 for i, ch
in save.characters
250 game, name, _slot = key
252 parents, restarts = build_tree(rows)
253 forks = fork_count(parents)
254 carried = carry_bosses(rows, parents)
255 for row, got
in zip(rows, carried):
256 row[
"carried_bosses"] = {b: ev
for b, (ev, _at)
in got.items()}
258 L = [f
"## {last['title']} — {name}",
"",
run_summary(rows, carried[-1]),
""]
259 L += [
"### Save Tree",
""]
261 "_Each box is one save file, numbered as in the references at the end. A "
262 "snapshot's parent is the latest earlier one whose progress it still "
263 "entirely contains — event flags never clear, so a fork (the same save "
264 "played on twice) lands both children on the shared ancestor._"
268 f
"{forks} fork{'' if forks == 1 else 's'}" if forks
else "No forks",
269 f
"{len(restarts)} separate line{'' if len(restarts) == 1 else 's'}"
272 "a dashed box is where a line stopped",
274 L.append(
"_" +
", ".join(n
for n
in note
if n) +
"._")
278 "_A box marked SEPARATE LINE could not descend from anything before it "
279 "— it holds less progress than saves that came earlier, so it belongs to "
280 "a different playthrough that happens to share this character's name and "
284 L += run_chart(rows, parents, restarts, refs, game)
287 L += [f
"### Current State — `{last['file']}` (^{refs.get(last['path'], '?')})",
""]
289 with open(last[
"path"],
"rb")
as f:
290 save = parse_save(f.read(), base_dir)
291 start = GAMES[save.game][
"slots"].start
292 for i, ch
in save.characters:
295 if i - start + 1 == last[
"slot"]:
299 ln
if not ln.startswith(
"#")
else "#" + ln
300 for ln
in md_for_character(ch, last[
"slot"]).split(
"\n")
303 except (OSError, ValueError, SystemExit):
304 L.append(
"_The newest save could not be re-read for a full dump._")
307 lost = carried_only(last, carried[-1])
310 "### Bosses Carried Forward",
312 "_Proven by an EARLIER save on this line and not by the newest one. A held "
313 "boss soul is proof of a kill, and spending the soul destroys the proof — "
314 "but a kill is permanent, so the evidence stands. Only this save's own "
315 "ancestors count; a boss killed on a different branch was never killed "
318 "| Boss | Evidence | Proven in | Play Time |",
321 for boss, ev, at
in lost:
324 f
"| {boss} | {', '.join(SRC.get(e, e) for e in ev)} | "
325 f
"^{refs.get(src['path'], '?')} | {hms(src['play_time'])} |"
331 L += [
"### Timeline",
""] + tl
341def build_combined(folder, base_dir, meta=None):
342 paths = folder
if isinstance(folder, list)
else find_saves(folder)
349 runs = group_runs(snaps)
350 refs, order = reference_index(snaps)
354 for rows
in runs.values():
355 parents, _restarts = build_tree(rows)
356 for row, got
in zip(rows, carry_bosses(rows, parents)):
357 row[
"carried_bosses"] = {b: ev
for b, (ev, _at)
in got.items()}
359 (s[
"title"],
None)
for s
in sorted(snaps, key=
lambda s: s[
"mtime"])
363 "# FromSoftware Saves — Combined Playthrough Timeline",
365 f
"_Reconstructed from {len(order)} save file{'' if len(order) == 1 else 's'} "
366 f
"across {len(runs)} run{'' if len(runs) == 1 else 's'} and "
367 f
"{len(games)} game{'' if len(games) == 1 else 's'}: "
371 "_Every timestamp is an UPPER BOUND, not the moment it happened: a thing is "
372 "dated to the first save it appears in, so the real event is somewhere between "
373 "the previous save and that one. This is a reconstruction from sparse backups, "
380 "_One box per character, in the order the files were last written — the only "
381 "clock the games share, since a Dark Souls II play time and a Dark Souls III "
382 "one are unrelated numbers._",
385 L += journey_chart(runs, refs)
388 for key, rows
in runs.items():
389 L +=
run_section(key, rows, refs, base_dir) + [
"",
"---",
""]
391 L += reference_list(order)
394 return "\n".join(L) +
"\n"
run_section(key, rows, refs, base_dir)
One run's whole section: chart, current state, timeline.
combined_footer(snaps, runs, meta)
The closing block: which games this document covers, how far to trust each, and any setup the caller ...