SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
sl2.timeline Namespace Reference

Functions

 estus_level (ch)
 
 snapshot (ch, path, slot_no, game, title)
 Flatten one parsed character into the fields a timeline needs.
 
 group_runs (snaps)
 Group snapshots into RUNS — one per character, across every file that holds it.
 
 flag_bosses (s)
 Boss kills that came from a FLAG, which is the only boss evidence that cannot go backwards.
 
 progress (s)
 The monotone progress a snapshot holds — the things that only ever grow.
 
 descends (a, b)
 Could b be a continuation of a — is everything a had still in b?
 
 build_tree (rows)
 Work out each snapshot's parent, turning a run into a forest.
 
 carry_bosses (rows, parents)
 Carry every boss kill forward down each line of descent.
 
 carried_only (row, carried)
 The bosses a snapshot can only prove through an ancestor, newest first.
 
 children (parents)
 Children of each node, in order.
 
 fork_count (parents)
 How many snapshots in this run have more than one child — the fork count.
 
 achievements (cur, prev, cap=3)
 What this snapshot achieved that its parent had not — the node's headline.
 
 reference_index (snaps)
 Number every file in the whole document, earliest to latest by FILE DATE.
 
 first_seen (rows, get)
 Generic first-seen walk: the earliest snapshot each item appears in.
 

Variables

 ESTUS_RE = re.compile(r"^Estus Flask(?: \+(\d+))?$")
 The Estus Flask's reinforcement level, or None if this character holds none.
 
tuple RESETTABLE = ("bonfires", "bosses", "cinders", "covenants")
 Progress that a New Game+ lap wipes — the per-map flags.
 

Detailed Description

Turn a pile of saves into runs, and each run into a tree of snapshots.

A folder of backups is not a list. Backups sorted by time LOOK linear, but reloading
an earlier save and playing on forks the run — the four Dark Souls III endings are
exactly that, one pre-ending save finished four different ways. This module works out
which snapshot descends from which, using the one thing every game in the series
guarantees: event flags never clear. If a save has a bonfire lit, every save after it
on the same line has that bonfire lit too, so a snapshot's parent is the latest
earlier one whose progress it still entirely contains — a sibling branch holds a flag
this one lacks, fails that test, and both land on the shared ancestor.

Nothing here renders. It reads parsed characters and returns data; sl2.chart draws it
and sl2.combine writes the document, so the inference can be tested without a document
and the document can change without touching the inference.

Function Documentation

◆ estus_level()

sl2.timeline.estus_level (   ch)

Definition at line 30 of file timeline.py.

Referenced by sl2.timeline.snapshot().

Here is the caller graph for this function:

◆ snapshot()

sl2.timeline.snapshot (   ch,
  path,
  slot_no,
  game,
  title 
)

Flatten one parsed character into the fields a timeline needs.

Bonfires are kept as (area, name) pairs so two areas sharing a bonfire name cannot collide in a first-seen set, and so every part of the document prints them the same way. Games that do not have a field simply do not get it — a DS1 character has no pickups and an Elden Ring one has no bonfires, and both are fine here.

Parameters
chA parsed character dict.
pathThe file it came from.
slot_noIts 1-based slot number.
gameThe game id,
titlethat game's display name.
Returns
A snapshot dict.

Definition at line 49 of file timeline.py.

References sl2.timeline.estus_level().

Here is the call graph for this function:

◆ group_runs()

sl2.timeline.group_runs (   snaps)

Group snapshots into RUNS — one per character, across every file that holds it.

The key is (game, character name, slot), not the file: a run is spread over dozens of backups, and one backup can hold several characters. The slot is in the key because an all-characters mule really does hold ten slots called the same thing, and merging those into one run would invent a history none of them had. A character that was moved to a different slot splits instead, which is the rarer mistake.

Ordering inside a run is by PLAY TIME, the game's own clock, falling back to the file date where a game does not store it (Elden Ring). That matters: file dates reorder when saves are copied around, play time does not.

Returns
OrderedDict {(game, name): [snapshot, ...]}, runs ordered by first appearance.

Definition at line 103 of file timeline.py.

◆ flag_bosses()

sl2.timeline.flag_bosses (   s)

Boss kills that came from a FLAG, which is the only boss evidence that cannot go backwards.

A boss known by its held soul disappears the moment the soul is consumed, and one inferred through a progression gate goes with it, so counting those as progress would fork the tree at every boss-soul spend.

Definition at line 118 of file timeline.py.

Referenced by sl2.timeline.progress().

Here is the caller graph for this function:

◆ progress()

sl2.timeline.progress (   s)

The monotone progress a snapshot holds — the things that only ever grow.

This is the whole basis for reconstructing lineage, so it contains only one-way signals. Souls are spent, a covenant is switched, embered is consumed and hollowing goes down with an effigy: any one of those would fork the tree on every death. Level, Estus and the flag-backed sets only ever climb.

Sekiro's Attack Power is the same kind of signal and it is the ONLY one that game offers: it has no level, no bonfires and no flags the tool can read, so without it a Sekiro run would have nothing to reconstruct a lineage from. A Memory consumed is never un-consumed, not even by a New Game+ lap.

Definition at line 133 of file timeline.py.

References sl2.timeline.flag_bosses().

Here is the call graph for this function:

◆ descends()

sl2.timeline.descends (   a,
  b 
)

Could b be a continuation of a — is everything a had still in b?

New Game+ is the one place a real continuation LOSES progress, so a journey bump waives the flags a lap resets. Endings are NOT waived: they accumulate across journeys, which is exactly what makes them the thing that separates two saves finished different ways from the same parent. Level and Estus never reset either.

Definition at line 163 of file timeline.py.

Referenced by sl2.timeline.build_tree().

Here is the caller graph for this function:

◆ build_tree()

sl2.timeline.build_tree (   rows)

Work out each snapshot's parent, turning a run into a forest.

A snapshot's parent is the LATEST earlier one it still contains, so a sibling branch is skipped over and both forks land on the shared ancestor.

When NOTHING earlier qualifies, the snapshot becomes a root of its own rather than being hung off whatever happened to precede it. That case is real and it is not an error: two characters with the same name in the same slot look like one run here, and a save that lost progress cannot be a continuation of anything before it. Drawing it as a second tree says "these are not the same line", which is what the data says; an edge would claim a descent that the flags refute.

Parameters
rowsSnapshots of one run, in order.
Returns
(parent index or None per row, indices of roots after the first).

Definition at line 196 of file timeline.py.

References sl2.timeline.descends().

Here is the call graph for this function:

◆ carry_bosses()

sl2.timeline.carry_bosses (   rows,
  parents 
)

Carry every boss kill forward down each line of descent.

A single save is a floor and it can only fall: the held-soul evidence that proves a kill DISAPPEARS the moment the soul is spent, so a later save reports fewer bosses than an earlier one on the same run. That is honest for one file — the save genuinely no longer proves it — but a document that has both files in front of it and still says "no evidence" is throwing away what it was given. A kill is permanent, so a boss proven at any ancestor is proven here.

ANCESTORS, not "every earlier snapshot": a sibling branch is a different line, and a boss killed there was never killed on this one. This is exactly the case the DS3 endings make real.

Parameters
rowsSnapshots of one run,
parentsfrom build_tree.
Returns
[{boss: (sorted evidence, index of the snapshot it was proven in)}, ...].

Definition at line 224 of file timeline.py.

◆ carried_only()

sl2.timeline.carried_only (   row,
  carried 
)

The bosses a snapshot can only prove through an ancestor, newest first.

Returns
[(boss, evidence, index of the snapshot that proved it)].

Definition at line 238 of file timeline.py.

◆ children()

sl2.timeline.children (   parents)

Children of each node, in order.

Returns
{parent index: [child index, ...]}.

Definition at line 247 of file timeline.py.

◆ fork_count()

sl2.timeline.fork_count (   parents)

How many snapshots in this run have more than one child — the fork count.

Definition at line 257 of file timeline.py.

◆ achievements()

sl2.timeline.achievements (   cur,
  prev,
  cap = 3 
)

What this snapshot achieved that its parent had not — the node's headline.

Ordered by how much it means, and capped, because a node has to stay readable: an ending outranks a boss, a boss outranks a bonfire, and "+3 bonfires" outranks a level-up. A snapshot that achieved nothing returns [], which is the honest answer for the many backups taken minutes apart.

Parameters
curThe snapshot,
previts parent (or None for the first).
capMost lines to return.
Returns
A list of short strings.

Definition at line 270 of file timeline.py.

◆ reference_index()

sl2.timeline.reference_index (   snaps)

Number every file in the whole document, earliest to latest by FILE DATE.

The reference list is what lets a node say "^12" instead of carrying a 40-character filename, and it is ordered by the file's modified time rather than play time because it spans games — the point of the ordering is "what did you play, in what order", which only the file date can answer.

Keyed by full PATH, never by name: every game writes to a fixed filename, so a folder of backups is full of files all called DS30000.sl2 and a name-keyed index would collapse them into one reference. Where the names really do collide the list shows enough of each path to tell them apart, and where they do not it stays short.

Parameters
snapsEvery snapshot in the document.
Returns
({path: number}, [(number, label, mtime, path), ...] in order).

Definition at line 371 of file timeline.py.

◆ first_seen()

sl2.timeline.first_seen (   rows,
  get 
)

Generic first-seen walk: the earliest snapshot each item appears in.

Parameters
getPulls the iterable of items from a snapshot.
Returns
[(item, snapshot), ...] in the order the items first showed up.

Definition at line 406 of file timeline.py.

Variable Documentation

◆ ESTUS_RE

sl2.timeline.ESTUS_RE = re.compile(r"^Estus Flask(?: \+(\d+))?$")

The Estus Flask's reinforcement level, or None if this character holds none.

Not a stored field. DS3 keeps the flask's level IN its goods id (two ids per level), so the parser resolves it to a name and the level rides in that name. Undead Bone Shards are the only thing that moves it, which is why it earns a timeline row. A +0 flask has no suffix, hence the optional group.

Definition at line 27 of file timeline.py.

◆ RESETTABLE

tuple sl2.timeline.RESETTABLE = ("bonfires", "bosses", "cinders", "covenants")

Progress that a New Game+ lap wipes — the per-map flags.

Bonfires go out, world pickups reset, the thrones empty, and the per-map boss flags clear (the cumulative victory flags do not, but a merged boss set cannot tell the two apart, so the whole set is treated as resettable rather than risk a false fork).

Definition at line 154 of file timeline.py.