SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
totals.py
Go to the documentation of this file.
1"""Progress denominators: what each section's "of N" is, and which entries are
2missing. Kept out of progress.py because naming the gap needs every game's own
3tables, which would otherwise make the two import each other.
4"""
5
6from .ds1 import load_ds1_boss_flags
7from .ds2 import DS2_COVENANT, DS2_GAMES, load_ds2_boss_souls, load_ds2_bosses
8from .ds3 import (
9 DS3_COVENANT,
10 load_ds3_boss_flags,
11 load_ds3_boss_victory,
12 load_ds3_covenants,
13)
14from .progress import (
15 BOSS_SOUL_DB_DIR,
16 DS3_CINDER_ITEM,
17 DS3_LORDS,
18 MANDATORY_BOSSES,
19 load_boss_route,
20 load_boss_soul_map,
21)
22from .sdt import load_sdt_boss_flags
23
24
25##
26# @brief Every boss this tool can name for a game — the denominator behind
27# "Bosses Defeated (8 of 26)".
28# @details Assembled from the game's own tables (defeat flags + boss-soul map), so it
29# is what the tool TRACKS, not a claim about how many bosses the game ships. That
30# distinction is the point: a name in here that is not in @c ch["bosses"] is a boss
31# the save shows no evidence for, which is worth printing; a boss no table knows
32# cannot be reported either way and must not inflate the denominator.
33def boss_roster(game, base_dir):
34 if game in DS2_GAMES:
35 return set(load_ds2_bosses(base_dir).values()) | set(
36 load_ds2_boss_souls(base_dir).values()
37 )
38 subdir = BOSS_SOUL_DB_DIR.get(game)
39 names = set(load_boss_soul_map(base_dir, subdir).values()) if subdir else set()
40 names |= set(MANDATORY_BOSSES.get(game, ()))
41 if game == "ds3":
42 names |= set(load_ds3_boss_flags(base_dir)) | set(
43 load_ds3_boss_victory(base_dir)
44 )
45 if game in ("dsr", "ptde"):
46 names |= set(load_ds1_boss_flags(base_dir))
47 if game == "sdt":
48 # The flag table's own names, even though its flags are not read yet: they are
49 # bosses the tool can NAME, which is exactly what the denominator counts. It
50 # adds the two the Memory table alone would miss (Emma and the ghost Monk).
51 names |= set(load_sdt_boss_flags(base_dir))
52 return names
53
54
55## @brief Every covenant a game has, as the denominator for "Covenants Found". These
56# are the in-game rosters (the id→name tables), so the count is the real total.
57def covenant_roster(game, base_dir):
58 if game in DS2_GAMES:
59 return set(DS2_COVENANT.values())
60 if game == "ds3":
61 return set(DS3_COVENANT.values()) | set(load_ds3_covenants(base_dir))
62 return set()
63
64
65##
66# @brief Attach the denominators the progress sections print against: how many bosses
67# and covenants the tool tracks, which of them this save shows nothing for, and
68# (DS3) how many Lords of Cinder are on the throne.
69# @details The Lords count is ARITHMETIC on two reads that are already verified, not a
70# new offset: a lord's cinders are in the inventory from the kill until they are
71# offered, so @c placed = lords defeated − cinders still held. It matches the mapped
72# throne flag on the one save that has one (the offering differential reads 1 defeated,
73# 0 held, 1 placed; the before-save reads 1 defeated, 1 held, 0 placed). Skipped on
74# NG+, where the thrones reset but the defeat flags do not, so the subtraction would
75# over-report. @param base_dir Repo root holding the db_* folders.
76def attach_progress_totals(ch, base_dir):
77 game = ch.get("game")
78 roster = boss_roster(game, base_dir)
79 if roster and ch.get("bosses"):
80 ch["boss_total"] = len(roster | set(ch["bosses"]))
81 ch["bosses_missing"] = sorted(n for n in roster if n not in ch["bosses"])
82 covs = covenant_roster(game, base_dir)
83 if covs and ch.get("covenants"):
84 ch["covenant_total"] = len(covs | set(ch["covenants"]))
85 ch["covenants_missing"] = sorted(n for n in covs if n not in ch["covenants"])
86 ch_bosses = ch.get("bosses") or {}
87 # Which of the missing bosses you could walk to RIGHT NOW: every hard predecessor
88 # dead, and at least one bonfire lit in its gate area (so a DLC boss cannot be
89 # "available" before you own/enter the DLC). Reached-area is the conservative half
90 # — it under-reports the very next area rather than sending you somewhere you
91 # cannot get to. Route structure only; nothing here is read from the save.
92 #
93 # A predecessor the game's tables cannot NAME is dropped rather than treated as
94 # alive, or it would withhold the boss for ever: DS1 can prove neither the Asylum
95 # Demon, the Gaping Dragon nor Pinwheel (no flag, no soul), and each of the three
96 # sits in front of an area whose own bonfire already proves you got past it —
97 # Taurus needs the Undead Burg, Quelaag needs Blighttown, Nito needs the Tomb of
98 # the Giants. So the area half carries the gate the roster cannot.
99 reached = {a for a, c, _n, _t, _m in (ch.get("bonfire_areas") or []) if c}
100 route = load_boss_route(base_dir, BOSS_SOUL_DB_DIR.get(game) or "")
101 if reached and route and roster:
102 # Only a boss the roster can name may be listed: "available" is a refinement of
103 # "missing", and a boss outside the roster is one the tool cannot say is alive.
104 avail = [
105 b
106 for b, (area, after) in route.items()
107 if b in roster
108 and b not in ch_bosses
109 and area in reached
110 and all(p in ch_bosses for p in after if p in roster)
111 ]
112 if avail:
113 ch["bosses_available"] = avail
114 if game != "ds3":
115 return
116 dead = [lord for boss, lord in DS3_LORDS.items() if boss in ch_bosses]
117 held = sum(q for n, q in (ch.get("key_items") or []) if n == DS3_CINDER_ITEM)
118 named = ch.get("cinders") or []
119 if dead or named:
120 lords = {
121 "total": len(DS3_LORDS),
122 "named": named,
123 "dead": len(dead),
124 "held": held,
125 }
126 # The subtraction only holds on a first journey; otherwise report just what the
127 # mapped throne flags prove, and say the count is a floor.
128 lords["placed"] = (
129 max(len(dead) - held, len(named)) if (ch.get("ng_plus") or 0) == 0 else None
130 )
131 ch["lords"] = lords
covenant_roster(game, base_dir)
Every covenant a game has, as the denominator for "Covenants Found".
Definition totals.py:57
boss_roster(game, base_dir)
Every boss this tool can name for a game — the denominator behind "Bosses Defeated (8 of 26)".
Definition totals.py:33