SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
progress.py
Go to the documentation of this file.
1"""Progress inference shared by every game: boss souls held, key items, the
2NG+ mandatory-clear floor, and the denominators each progress section prints.
3"""
4
5import json
6import os
7from collections import OrderedDict
8
9## @brief Ordinary soul consumables that are NOT boss souls. A boss soul in your
10# pack means the boss is dead; a "Soul of a Lost Undead" just means you
11# killed something ordinary.
12GENERIC_SOULS = {
13 "Fading Soul",
14 "Soul of a Lost Undead",
15 "Large Soul of a Lost Undead",
16 "Soul of a Nameless Soldier",
17 "Large Soul of a Nameless Soldier",
18 "Soul of a Proud Knight",
19 "Large Soul of a Proud Knight",
20 "Soul of a Brave Warrior",
21 "Large Soul of a Brave Warrior",
22 "Soul of a Hero",
23 "Soul of a Great Hero",
24 "Soul of a Old Hero",
25 "Wandering Soul",
26 "Old Soul",
27 # Dark Souls III generic farm souls — not bosses.
28 "Soul of a Deserted Corpse",
29 "Large Soul of a Deserted Corpse",
30 "Soul of an Unknown Traveler",
31 "Large Soul of an Unknown Traveler",
32 "Soul of a Weary Warrior",
33 "Large Soul of a Weary Warrior",
34 "Soul of a Crestfallen Knight",
35 "Large Soul of a Crestfallen Knight",
36 "Soul of a Venerable Old Hand",
37 "Soul of a Champion",
38 "Soul of a Great Champion",
39 "Soul of a Seasoned Warrior",
40 "Large Soul of a Seasoned Warrior",
41 "Soul of an Intrepid Hero",
42 "Large Soul of an Intrepid Hero",
43}
44
45
46## @brief DS1 progression goods that gate the world but do not read as "keys".
47# Crest of Artorias opens the sealed Darkroot door, so it gates as hard as a key.
48DS1_PROGRESSION = {
49 "Lordvessel",
50 "Peculiar Doll",
51 "Broken Pendant",
52 "Rite of Kindling",
53 "Crest of Artorias",
54}
55
56
57## @brief Pull the likely boss / lord souls out of a goods list.
58def find_boss_souls(goods):
59 out = []
60 for n, q in goods:
61 if n in GENERIC_SOULS:
62 continue
63 if (
64 "Soul of " in n
65 or "Lord Soul" in n
66 or n in ("Core of an Iron Golem", "Guardian Soul")
67 ):
68 out.append((n, q))
69 return out
70
71
72## @brief Per-game folder holding a boss-soul → boss-name table (boss_souls.json).
73# DS2 runs its own richer multi-source inference; these cover the games whose only
74# proof-of-kill floor is the boss souls / remembrances the character still holds.
75# Sekiro's table maps its Memory items, which are the same kind of token — and the
76# only one in the series whose spending leaves a trace (see sdt_memories_spent).
77BOSS_SOUL_DB_DIR = {
78 "dsr": "db_ds1",
79 "ptde": "db_ds1",
80 "ds3": "db_ds3",
81 "er": "db_er",
82 "sdt": "db_sdt",
83}
84
85
86## @brief Load a game's boss-soul → boss-name table. Cached per (base_dir, subdir).
87_BOSS_SOUL_CACHE = {}
88
89
90def load_boss_soul_map(base_dir, subdir):
91 key = (base_dir, subdir)
92 if key not in _BOSS_SOUL_CACHE:
93 path = os.path.join(base_dir, subdir, "boss_souls.json")
94 try:
95 with open(path, encoding="utf-8") as f:
96 _BOSS_SOUL_CACHE[key] = json.load(f)
97 except (OSError, ValueError):
98 _BOSS_SOUL_CACHE[key] = {}
99 return _BOSS_SOUL_CACHE[key]
100
101
102## @brief Load a game's route graph (boss_route.json): boss name →
103# @c [gate area, [bosses that must die first]]. GAME STRUCTURE, not a save read —
104# it exists to answer "what can I fight now", which no flag can. The area name is a
105# key of that game's own bonfire table, so "the area was reached" is decided by that
106# area's lit bonfires; the predecessor list is only the HARD gates (the arena or key
107# you cannot get past otherwise), never a suggested order.
108# Cached per (base_dir, subdir). Returns {} if the game has no table.
109_BOSS_ROUTE_CACHE = {}
110
111
112def load_boss_route(base_dir, subdir):
113 key = (base_dir, subdir)
114 if key not in _BOSS_ROUTE_CACHE:
115 path = os.path.join(base_dir, subdir, "boss_route.json")
116 try:
117 with open(path, encoding="utf-8") as f:
118 _BOSS_ROUTE_CACHE[key] = json.load(f)
119 except (OSError, ValueError):
120 _BOSS_ROUTE_CACHE[key] = {}
121 return _BOSS_ROUTE_CACHE[key]
122
123
124## @brief Endgame-only progression prereqs: a proven-dead boss (key) implies its
125# mandatory predecessors (values) are dead too, tagged `gate`. Each key lists ALL
126# its predecessors (already flattened), so one closure pass suffices. DELIBERATELY
127# ENDGAME-ONLY, mirroring DS2's `DS2_BOSS_PREREQ` rule: only strictly-linear,
128# cannot-skip mandatory chains qualify — a mid-game gate would risk a false kill
129# (the core rule). Sourced from each game's fixed endgame route.
130#
131# DS3: the four Lords of Cinder plus Iudex Gundyr are all mandatory to fight Soul
132# of Cinder, and Vordt/Dancer gate the only path forward (High Wall → … → Lothric
133# Castle). Aldrich sits past Pontiff Sulyvahn in Irithyll.
134# ER: Morgott (Leyndell) → Fire Giant (Forge) → Maliketh (Farum Azula) → Godfrey/
135# Hoarah Loux (Ashen Leyndell) is the fixed mandatory endgame chain; each requires
136# every earlier one. Morgott's own prereqs are player-choice great runes, so it
137# gates nothing specific.
138BOSS_PREREQ = {
139 "ds3": {
140 "Soul of Cinder": [
141 "Iudex Gundyr",
142 "Vordt of the Boreal Valley",
143 "Dancer of the Boreal Valley",
144 "Abyss Watchers",
145 "Aldrich, Devourer of Gods",
146 "Yhorm the Giant",
147 "Lothric, Younger Prince",
148 ],
149 "Lothric, Younger Prince": [
150 "Dancer of the Boreal Valley",
151 "Vordt of the Boreal Valley",
152 "Iudex Gundyr",
153 ],
154 "Aldrich, Devourer of Gods": [
155 "Pontiff Sulyvahn",
156 "Vordt of the Boreal Valley",
157 "Iudex Gundyr",
158 ],
159 "Dancer of the Boreal Valley": ["Vordt of the Boreal Valley", "Iudex Gundyr"],
160 "Pontiff Sulyvahn": ["Vordt of the Boreal Valley", "Iudex Gundyr"],
161 "Vordt of the Boreal Valley": ["Iudex Gundyr"],
162 },
163 "er": {
164 "Godfrey, First Elden Lord (Hoarah Loux)": [
165 "Maliketh, the Black Blade",
166 "Fire Giant",
167 "Morgott, the Omen King",
168 ],
169 "Maliketh, the Black Blade": ["Fire Giant", "Morgott, the Omen King"],
170 "Fire Giant": ["Morgott, the Omen King"],
171 },
172}
173
174
175## @brief Bosses that CANNOT be skipped to finish the game, so **reaching NG+ proves
176# every one of them dead at least once** (tag `clear`). DS1 (dsr/ptde) is linear from
177# Anor Londo on: both bells (Gargoyles, Quelaag), Sen's/Anor Londo (Iron Golem, O&S),
178# the four Lord Souls (Nito, Bed of Chaos, Four Kings — needs Sif's ring — and Seath)
179# and Gwyn are all mandatory. Deliberately endgame-safe, the same core-rule caution as
180# the gate maps — no mid-game boss whose route can be skipped is listed. (DS2 handles
181# this itself in ds2_infer_bosses, seeding only its final boss Nashandra, because DS2's
182# mid-game is skippable — Shrine of Winter opens on Soul Memory alone.)
183MANDATORY_BOSSES = {
184 "dsr": [
185 "Bell Gargoyles",
186 "Chaos Witch Quelaag",
187 "Iron Golem",
188 "Dragon Slayer Ornstein",
189 "Executioner Smough",
190 "Great Grey Wolf Sif",
191 "The Four Kings",
192 "Seath the Scaleless",
193 "Gravelord Nito",
194 "Bed of Chaos",
195 "Gwyn, Lord of Cinder",
196 ],
197}
198
199
200MANDATORY_BOSSES["ptde"] = MANDATORY_BOSSES["dsr"]
201
202
203## DS3's unskippable path to Soul of Cinder — the four Lords of Cinder plus the
204## bosses that gate them (Iudex/Vordt/Dancer/Pontiff/Dragonslayer Armour). Reaching
205## NG+ proves all of these dead even if their souls were spent. Optional bosses
206## (Greatwood, Crystal Sage, Wolnir, Nameless King…) are deliberately excluded.
207MANDATORY_BOSSES["ds3"] = [
208 "Iudex Gundyr",
209 "Vordt of the Boreal Valley",
210 "Dancer of the Boreal Valley",
211 "Abyss Watchers",
212 "Pontiff Sulyvahn",
213 "Aldrich, Devourer of Gods",
214 "Yhorm the Giant",
215 "Dragonslayer Armour",
216 "Lothric, Younger Prince",
217 "Soul of Cinder",
218]
219
220
221## @brief Attach a `bosses` defeat floor to a non-DS2 character from the boss souls
222# / remembrances it still holds, plus endgame progression. A held boss soul is a
223# boss killed — you cannot own the soul otherwise — so each maps to its boss with
224# `soul` evidence, and its mandatory endgame predecessors get `gate` (see
225# BOSS_PREREQ), the same certain-when-true signals DS2 uses. If the character is in
226# NG+ (ng_plus > 0) every MANDATORY_BOSSES entry is proven dead too (`clear`). A boss
227# whose soul was consumed, not gated and not mandatory, is invisible here (the render
228# note says so). DS2 sets its own richer `bosses` via augment and is skipped.
229def attach_defeated_bosses(ch, base_dir):
230 game = ch.get("game")
231 subdir = BOSS_SOUL_DB_DIR.get(game)
232 if not subdir or ch.get("bosses"):
233 return
234 soul_db = load_boss_soul_map(base_dir, subdir)
235 bosses = {}
236 for name, _q in ch.get("boss_souls") or []:
237 boss = soul_db.get(name)
238 if boss:
239 bosses.setdefault(boss, set()).add("soul")
240 if (ch.get("ng_plus") or 0) > 0:
241 for boss in MANDATORY_BOSSES.get(game, ()):
242 bosses.setdefault(boss, set()).add("clear")
243 prereq = BOSS_PREREQ.get(game, {})
244 for boss in list(bosses):
245 for pre in prereq.get(boss, ()):
246 bosses.setdefault(pre, set()).add("gate")
247 if bosses:
248 # Sort each evidence set for a stable render order (matches ds2_infer_bosses,
249 # which already sorts); boss keys keep insertion order.
250 ch["bosses"] = {b: sorted(bosses[b]) for b in bosses}
251
252
253## @brief The four Lords of Cinder, boss name → the name on the Firelink throne.
254# A closed set: all four must be placed before the Kiln opens, so "N of 4" is a real
255# denominator rather than an open-ended list.
256DS3_LORDS = OrderedDict(
257 [
258 ("Abyss Watchers", "Abyss Watchers"),
259 ("Yhorm the Giant", "Yhorm the Giant"),
260 ("Aldrich, Devourer of Gods", "Aldrich"),
261 ("Lothric, Younger Prince", "Twin Princes"),
262 ]
263)
264
265
266## @brief The one item id (any lord's) that sits in the inventory between killing a
267# Lord of Cinder and offering the ashes at the throne.
268DS3_CINDER_ITEM = "Cinders of a Lord"
269
270
271## @brief Pull key / progression items out of a goods list (DS1 keeps keys here).
272def find_key_goods(goods):
273 return [(n, q) for n, q in goods if "Key" in n or n in DS1_PROGRESSION]