SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
Loading...
Searching...
No Matches
sdt.py
Go to the documentation of this file.
1"""Sekiro: Shadows Die Twice.
2
3The odd one out in three ways, and each of them makes the read simpler rather than
4harder. The save is NOT encrypted (plaintext BND4 entries behind a plain MD5, like
5PtDE and Elden Ring), the slot offsets do NOT move between patches (so there is no
6content scan and no level identity to check), and reinforcement is NOT baked into an
7item id — a prosthetic tool's upgrade tier is its own id, so a straight lookup names
8"Lazulite Shuriken" with no arithmetic.
9
10What it does not have is a character name — Sekiro's profiles are unnamed by design —
11or attributes. Its numbers are Attack Power, max HP and max Posture, and the two upgrade
12levels behind them: Vitality and the Healing Gourd. Neither of the last two is in any
13published source; both were pinned here by differentials, and both are spent-token
14counters — the item that raised them is consumed, so the level is the only record that
15it was ever held.
16
17Offsets from uberhalit/SimpleSekiroSavegameHelper (the container) and
18alfizari/Sekiro-Save-Editor (the slot fields), every one of them verified against a real
19S0000.sl2 — and two of that editor's labels corrected against a differential, because
20they point at the current value rather than the maximum.
21"""
22
23import json
24import os
25
26from .reader import u8, u32, u64
27
28## @brief How many character slots the game has. Entry 10 is the settings/profile
29# block and entry 11 (present on the current patch, absent in the published layout)
30# is reserved and reads all zeros.
31SDT_SLOT_COUNT = 10
32
33
34##
35# @brief Slot fields, at fixed offsets into the decrypted (plaintext) slot payload.
36# @details These do not move between patches — Sekiro has no equivalent of the DS3
37# stat block that drifts — so they are read directly rather than searched for. The
38# Steam id is NOT printed anywhere; it is read only as the occupancy test, because an
39# unused slot is all zeros and a used one carries its owner's id. That was checked
40# both ways: the id matches the save's own folder name, and the nine unused slots (and
41# a second, characterless save file) read zero.
42SDT_STEAM_OFF = 0x33E54 # u64
43SDT_NG_OFF = 0x33F34 # u8, journey (New Game+) count
44SDT_PLAYTIME_OFF = 0x33F80 # u32, SECONDS
45SDT_ATTACK_OFF = 0x3449C # u32, attack power
46SDT_SEN_OFF = 0x344D0 # u32, Sen (the currency — alfizari's README calls it Souls)
47
48
49##
50# @brief Max HP and max Posture, each stored TWICE, and neither at the offset the
51# published source names.
52# @details Both live in the same four-word shape — @c [0][current][max][max] — and
53# alfizari's editor labels the CURRENT field of each as the maximum. A real differential
54# settles it: across a 42-minute window the word at @c 0x3446C moved 32 → 160 while
55# @c 0x34470 and @c 0x34474 both held at 320. A value that moves while its neighbours do
56# not is the current one; the pair that holds is the maximum. Posture sits in the
57# identical shape one group along (its zero word at @c 0x34484), so its maximum is the
58# same two fields over — which also explains why all three of its words read 120: posture
59# is a pool that depletes, and an undamaged character is at full.
60#
61# Each is read only where BOTH copies agree and the value is nonzero. That is the same
62# self-consistency gate the DS3 equipment slots use, and here it costs nothing: the game
63# writes both, so a disagreement means the read landed somewhere it should not have.
64SDT_HP_OFF, SDT_HP_ALT = 0x34470, 0x34474
65SDT_POSTURE_OFF, SDT_POSTURE_ALT = 0x3448C, 0x34490
66
67
68##
69# @brief Vitality, the second of Sekiro's two upgrade tracks, at the word immediately
70# before Attack Power.
71# @details In no published source at all — it was pinned by the differential the old
72# blocker asked for. A 21-second window in which the character used four Prayer Beads
73# (the First Prayer Necklace) moved Max HP 320 → 400 and Max Posture 120 → 150, and in
74# the whole player struct exactly ONE other word moved: this one, 1 → 2. Every earlier
75# save on the same ladder reads 1 with no necklace used, and a characterless save reads
76# 0, so the field is not a coincidence of that one window.
77#
78# The stored value IS the number the status screen shows — no base to subtract, unlike
79# @ref SDT_ATTACK_BASE, because a fresh character reads 1 and one necklace makes it 2.
80# The ceiling is the in-game cap; past it the read landed somewhere it should not have.
81SDT_VITALITY_OFF = 0x34498
82SDT_VITALITY_MAX = 20
83
84
85##
86# @brief The Healing Gourd's CHARGE COUNT — a byte, and Sekiro's third spent-token
87# counter — at @c 0x34562.
88# @details In no published source either, and pinned the same way Vitality was. A
89# four-minute window in which one Gourd Seed was handed to Emma moved exactly one byte
90# in the whole player struct: this one, 7 → 8. The other five words that moved in the
91# same window are play time, two timers and a pair that only tracks with play time,
92# none of them a small monotone count.
93#
94# Two independent checks across the whole 45-save ladder say what the byte MEANS, and
95# they disagree with the obvious first reading of "seeds used":
96#
97# - **Conservation.** A seed is consumed the moment Emma takes it, so `this byte + the
98# Gourd Seeds still in the inventory` must never fall. It never does, over 45 saves,
99# and on each save where a held seed disappears the byte takes exactly the +1 the
100# inventory lost. A wrong offset cannot stay conserved against a list parsed
101# independently of it.
102# - **It is the capacity, not the tally.** The Healing Gourd is an ordinary inventory
103# row whose quantity is its remaining charges, and that quantity never exceeds this
104# byte and repeatedly equals it — 4 on all eight saves at 4, 5 at 5, 7 at 7. Were the
105# byte a count of seeds with charges some fixed amount above it, a gourd refilled at
106# every idol would have to read higher than this somewhere in 45 saves. It never does.
107#
108# So the byte IS the number of charges, it starts at 1 (a characterless save and every
109# unused slot read 0; the earliest save on the ladder reads 1 with a one-charge gourd),
110# and one seed buys one charge — which makes `value - 1` the seeds consumed, cumulative
111# across journeys like Attack Power and Vitality. The ceiling is the game's own: the
112# @c maxNum of @c EquipParamGoods row 3000, read out of this machine's install.
113SDT_GOURD_OFF = 0x34562
114SDT_GOURD_MAX = 10
115
116
117##
118# @brief Read a field the game stores twice, or None unless both copies agree.
119# @param buf The slot payload. @param off The field. @param alt Its second copy.
120def sdt_twin(buf, off, alt):
121 value = u32(buf, off)
122 return value if value and value == u32(buf, alt) else None
123
124
125## @brief Attack power on a character who has consumed no Memory. Read off a real
126# save that is minutes from the opening — no Memory held, no gourd, one key item —
127# which is what makes the base a measurement rather than an assumption. It matters
128# because @ref sdt_memories_spent subtracts it.
129SDT_ATTACK_BASE = 1
130
131
132## @brief Ceilings that make a field a field rather than noise: a value past either
133# means the read landed somewhere it should not have, and the field is dropped.
134#
135# Attack power was 98 here and that was one too low — a real journey-9 save reads
136# exactly 99, so the tool was silently dropping Attack Power, and with it the Memories
137# line, on precisely the characters that have the most of it. Raised on that evidence.
138# The gate exists to reject a read in the thousands, not to tell 98 from 99.
139#
140# Note the Memories arithmetic UNDERCOUNTS at the cap, because Attack Power stops
141# rising while the kills do not. It is a floor, which is what the line already says.
142SDT_ATTACK_MAX, SDT_NG_MAX = 99, 99
143
144
145## @brief One inventory-style list: where it starts and how long it runs. Records are
146# 16 bytes, `[u32 handle][u32 item id][u32 quantity][u32 index]`. `key` items get
147# their own region in Sekiro (every other game needs them filtered out of a bucket),
148# and the storage box is kept apart because an item in the box is owned but not
149# carried — a distinction the report can only make if the read makes it too.
150SDT_LISTS = (
151 ("inv", 0x8F70C, 0x7000),
152 ("key", 0x9670C, 0x2000),
153 ("storage", 0x987A0, 0x9000),
154 ("storage", 0xA1958, 0x4000),
155)
156
157
158SDT_RECORD = 16
159
160
161## @brief Item type, from the top nibble of the record's HANDLE. Scoped by
162# construction, the same property Elden Ring's id nibble gives: an armour handle
163# cannot resolve to a weapon of the same number. 0x0 is an empty record.
164SDT_CAT = {0x8: "weapons", 0x9: "armors", 0xB: "goods"}
165
166
167## @brief The item id proper is the low 24 bits; the rest is the type code.
168SDT_ID_MASK = 0x00FFFFFF
169
170
171##
172# @brief Category refinement for the weapons table, applied at load time.
173# @details Sekiro keeps combat arts, prosthetic tools and a pile of engine-internal
174# rows in one param table, so one heading for all of them would be useless. The split
175# is by id block, read off the table sorted rather than guessed: the prosthetic tools
176# are exactly the ids in @c prosthetics.json (the 7xxxx range, one id per upgrade
177# tier), the combat arts are the five-digit ids below them, and everything above is
178# the skill tree plus the "Virtual Weapon:" / "Upgrade Menu:" internals.
179SDT_ARTS_MAX = 9999
180
181
182def sdt_weapon_cat(iid, prosthetics):
183 if iid in prosthetics:
184 return "prosthetics"
185 return "arts" if iid <= SDT_ARTS_MAX else "skills"
186
187
188##
189# @brief Category refinement for the goods table, by id block.
190# @details Same method as @ref sl2.ds3.ds3_goods_cat and the same justification: the
191# ids block out cleanly by kind when the table is read in order, so the report can
192# mirror the in-game menu instead of dumping 279 rows under one heading. Ranges are
193# inclusive on both ends.
194SDT_GOODS_RANGES = (
195 (500, 1999, "consumables"), # Spirit Emblem, Regenerative Power, Skill Point
196 (2000, 2999, "key"), # Kusabimaru, Mortal Blade, the esoteric texts
197 (3000, 3999, "consumables"), # gourds, sugars, spiritfall, confetti, shards
198 (4000, 4499, "beads"), # Prayer Bead, the ten necklaces, Gourd Seed
199 (5100, 5499, "memories"), # Memory: / Remnant: — the boss tokens
200 (5500, 5999, "key"), # Mask Fragments, Dragon's Blood Droplet
201 (6000, 6999, "upgrade"), # scrap iron, gunpowder, wax, lapis lazuli
202 (9000, 9999, "key"), # quest items, notes, Rot Essence, shop scrolls
203)
204
205
206def sdt_goods_cat(iid):
207 for lo, hi, cat in SDT_GOODS_RANGES:
208 if lo <= iid <= hi:
209 return cat
210 return "goods"
211
212
213##
214# @brief Rows that resolve to a real name but are engine state, not inventory.
215# @details Sekiro has no armour system, so `EquipParamProtector` is not an equipment
216# table at all — it is the model list for the character's own body. Every row that is
217# one of Wolf's own parts (`Original Memory: Wolf - Head`, and the cutscene rig beside
218# it) is the engine recording which mesh is on, and printing them gave every export an
219# `#### Armor` heading listing the player's limbs.
220#
221# The `Another's Memory:` blocks — Shura, Ashina, Tengu — are NOT suppressed, and that
222# is deliberate rather than cautious. They look like the three unlockable attires, and
223# no save here has one unlocked, so suppressing them would be a claim about a channel
224# this repo has never observed carrying anything. Leaving them in costs nothing: the
225# heading is only emitted when a row survives, so a character who has none prints no
226# section, and an NG+ save will simply show them if they do surface. If they never do,
227# nothing was lost either way.
228#
229# `Virtual Weapon:` restates a Combat Art already listed under its own name, and
230# `Upgrade Menu:` rows are prosthetic upgrade-tree entries. Both are duplicates of a
231# real row rather than items.
232#
233# Suppressed rows are counted, not hidden — see @c suppressed_count.
234SDT_SUPPRESSED_PREFIXES = (
235 "Original Memory:",
236 "Immortal Severance Cutscene",
237 "Virtual Weapon:",
238 "Upgrade Menu:",
239)
240
241
242def sdt_suppressed(name):
243 return name.startswith(SDT_SUPPRESSED_PREFIXES)
244
245
246## @brief The two goods rows both named plain "Memory" are engine COUNTERS, not tokens.
247# Read across the 60-save ladder: `5100`'s quantity is the number of Memories consumed
248# (it tracks Attack Power − 1 on every save, through NG+ on the finished one), and
249# `5400` sits at 1 exactly while a boss Memory is carried unspent. Counting them as
250# held Memories is what made a character with nothing to spend report one "still held",
251# and listing them printed `Memory ×5` in an inventory that holds no Memory at all.
252SDT_MEMORY_COUNTER_IDS = (5100, 5400)
253## @brief The boss Memories themselves (`Memory: Gyoubu Oniwa` …), the only rows that
254# are a token in hand. The game files them in the key-item list.
255SDT_BOSS_MEMORY_IDS = (5200, 5299)
256
257
258## @brief Skill points are a spendable currency, not a consumable, so the report puts
259# them beside Attack Power and Vitality instead of in with the sugars and the gourds.
260SDT_SKILL_POINT_ID = 1200
261
262
263## @brief The db_sdt files that carry real English names, one per item type.
264SDT_DB_FILES = ("weapons", "armors", "goods")
265
266
267##
268# @brief Load the Sekiro id tables, type-scoped: @c {category: {id: name}}.
269# @details A fourth id scheme, and the simplest of the four: decimal id keys, one file
270# per type, no category collisions to design around because the handle nibble already
271# says which table to look in.
272#
273# The @c *_devnames.json files are loaded into their own table and never merged. They
274# hold Paramdex's machine-translated Japanese dev strings for the ids with no English
275# name ("ID monitoring item 1", "Right-handed sword_style: None"), and most of them are
276# engine internals a player never sees — merging them would put debug rows in the
277# inventory under the same heading as real items. Anything that resolves only there is
278# reported as an internal entry instead, so it is neither hidden nor dressed up.
279# @param db_dir Folder holding the tables.
280# @return @c {"names": {cat: {id: name}}, "dev": {cat: {id: name}}, "prosthetics": set}.
281def load_sdt_db(db_dir):
282 def table(stem):
283 try:
284 with open(os.path.join(db_dir, stem + ".json"), encoding="utf-8") as f:
285 return {int(k): v for k, v in json.load(f).items()}
286 except (OSError, ValueError):
287 return {}
288
289 names = {cat: table(cat) for cat in SDT_DB_FILES}
290 dev = {cat: table(cat + "_devnames") for cat in SDT_DB_FILES}
291 return {"names": names, "dev": dev, "prosthetics": set(table("prosthetics"))}
292
293
294##
295# @brief Resolve one item id to @c (name, category, internal).
296# @details The type nibble decides which table is consulted, so a lookup cannot cross
297# types. A hit in the named table gives the render category; a hit only in the dev-name
298# table is flagged @c internal, which is how the caller knows to count it rather than
299# print it. A miss in both is left to the caller to count as unknown.
300# @param cat The type from the handle nibble (weapons/armors/goods).
301# @param iid The masked item id.
302# @param db The bundle from @ref load_sdt_db.
303def sdt_resolve(cat, iid, db):
304 name = db["names"].get(cat, {}).get(iid)
305 if name is not None:
306 if cat == "weapons":
307 return name, sdt_weapon_cat(iid, db["prosthetics"]), False
308 if cat == "goods":
309 return name, sdt_goods_cat(iid), False
310 return name, cat, False
311 name = db["dev"].get(cat, {}).get(iid)
312 return (name, "internal", True) if name is not None else (None, None, False)
313
314
315##
316# @brief Walk Sekiro's four item lists.
317# @details Each region is a flat array of 16-byte records and the game leaves the tail
318# zeroed, so an empty record (handle 0) is skipped rather than treated as the end —
319# the count that matters is which records carry a known type nibble. That gate is what
320# keeps a mis-located region from inventing items: random bytes clear it three times in
321# sixteen, and a zeroed tail never clears it at all.
322# @param buf The slot payload. @param db The bundle from @ref load_sdt_db.
323# @return A generator of @c (which list, item id, name, category, quantity, internal).
324def sdt_items(buf, db):
325 for which, start, length in SDT_LISTS:
326 for off in range(start, start + length, SDT_RECORD):
327 handle = u32(buf, off)
328 if not handle:
329 continue
330 cat = SDT_CAT.get((handle >> 28) & 0xF)
331 if cat is None:
332 continue
333 iid = u32(buf, off + 4)
334 if iid is None:
335 continue
336 iid &= SDT_ID_MASK
337 name, render_cat, internal = sdt_resolve(cat, iid, db)
338 qty = u32(buf, off + 8)
339 yield which, iid, name, render_cat, qty, internal
340
341
342##
343# @brief How many Memories this character has consumed, or None.
344# @details Sekiro's boss tokens are Memories, and consuming one at an idol raises
345# Attack Power by exactly one — so a stored Attack Power is a COUNT of the Memories
346# already spent, which is the one thing no other game in this repo can recover. Every
347# other boss floor here goes blind the moment the token is used; this one does not.
348#
349# Two honest limits, both stated where it is rendered. The base is 1, not 0
350# (@ref SDT_ATTACK_BASE), and Attack Power carries across New Game+ while the Memories
351# do not, so past journey 0 the number counts every lap rather than this one.
352# @param attack The stored attack power, or None. @return The count, or None.
353def sdt_memories_spent(attack):
354 if attack is None or not SDT_ATTACK_BASE <= attack <= SDT_ATTACK_MAX:
355 return None
356 return attack - SDT_ATTACK_BASE
357
358
359##
360# @brief Parse one Sekiro slot into the unified character dict.
361# @details No name and no attributes: both are absent from the game, not missing from
362# the tool, and the report says so rather than leaving a blank. An unused slot is all
363# zeros, which is how a populated one is told apart — the game publishes no occupancy
364# array, so the test is the slot's own content.
365#
366# Max HP and max Posture are read from the second of each field's two copies, NOT from
367# the offset alfizari names — see @ref sdt_twin for the differential that settles it.
368#
369# **Spirit emblems** is still deliberately NOT read. The documented @c uint16 at
370# @c 0x3459A reads 15 on a character who owns no Spirit Emblem item and has no prosthetic
371# at all, and stayed 15 across a window in which the inventory grew by four items — so it
372# is the carry CAP (which starts at 15), not the count. It needs a save either side of
373# actually spending emblems, and a wrong number is worse than none.
374# @param buf The slot payload. @param db The bundle from @ref load_sdt_db.
375# @return A unified character dict, or None if the slot is empty.
376def sdt_parse(buf, db):
377 inv, key_items, memories, unknown, internal = {}, [], [], 0, 0
378 suppressed, skill_points = 0, None
379 for which, iid, name, cat, qty, is_internal in sdt_items(buf, db):
380 if name is None:
381 unknown += 1
382 continue
383 if is_internal:
384 internal += 1
385 continue
386 if sdt_suppressed(name) or (
387 cat == "memories" and iid in SDT_MEMORY_COUNTER_IDS
388 ):
389 suppressed += 1
390 continue
391 # A skill point is spendable currency, so it rides in the header beside the
392 # other two upgrade tracks rather than in with the sugars. Only a CARRIED one
393 # is promoted — a copy sitting in the box is genuinely in the box, and the
394 # storage list would otherwise lose it with nothing saying so.
395 if iid == SDT_SKILL_POINT_ID and which != "storage":
396 skill_points = (skill_points or 0) + (qty or 0)
397 continue
398 row = (name, qty)
399 # A boss Memory is the kill's proof wherever it is listed, and the game lists
400 # it with the key items — so it is collected before the list decides placement.
401 if (
402 cat == "memories"
403 and SDT_BOSS_MEMORY_IDS[0] <= iid <= SDT_BOSS_MEMORY_IDS[1]
404 ):
405 memories.append(row)
406 # The box wins over the category: a key item sitting in storage is in
407 # storage, and saying otherwise would report it as carried.
408 if which == "storage":
409 inv.setdefault("storage", []).append(row)
410 elif which == "key" or cat == "key":
411 key_items.append(row)
412 else:
413 inv.setdefault(cat, []).append(row)
414
415 play_time = u32(buf, SDT_PLAYTIME_OFF)
416 steam_id = u64(buf, SDT_STEAM_OFF)
417 if not any((inv, key_items, play_time, steam_id)):
418 return None
419
420 attack = u32(buf, SDT_ATTACK_OFF)
421 if attack is not None and attack > SDT_ATTACK_MAX:
422 attack = None
423 ng = u8(buf, SDT_NG_OFF)
424 vitality = u32(buf, SDT_VITALITY_OFF)
425 if vitality is not None and not 1 <= vitality <= SDT_VITALITY_MAX:
426 vitality = None
427 gourd = u8(buf, SDT_GOURD_OFF)
428 if gourd is not None and not 1 <= gourd <= SDT_GOURD_MAX:
429 gourd = None
430 ch = {
431 "tier": "full",
432 "game": "sdt",
433 # Sekiro profiles carry no name — the game never asks for one — so the slot
434 # is labelled the same way an empty Elden Ring roster entry is. What that
435 # means is said once, in SDT_NOTE, rather than inside every heading and
436 # every chart box that has to carry the name.
437 "name": "(unnamed)",
438 "klass": None,
439 "stats": {},
440 "level": None,
441 "soul_memory": None,
442 "humanity": None,
443 "stamina": None,
444 "hp": sdt_twin(buf, SDT_HP_OFF, SDT_HP_ALT),
445 "posture": sdt_twin(buf, SDT_POSTURE_OFF, SDT_POSTURE_ALT),
446 "ng_plus": ng if ng is not None and ng <= SDT_NG_MAX else None,
447 "play_time": play_time,
448 "souls": u32(buf, SDT_SEN_OFF),
449 "attack": attack,
450 "vitality": vitality,
451 "gourd": gourd,
452 "skill_points": skill_points,
453 "boss_souls": memories,
454 "key_items": key_items,
455 "inv": inv,
456 "unknown_count": unknown,
457 "internal_count": internal,
458 "suppressed_count": suppressed,
459 }
460 spent = sdt_memories_spent(attack)
461 if spent is not None:
462 ch["memories"] = {
463 "spent": spent,
464 "held": len(memories),
465 "cumulative": bool(ch["ng_plus"]),
466 }
467 return ch
468
469
470##
471# @brief Load the Sekiro boss-defeat flag table (boss name → event flag id).
472# @details Also the source of the "of N tracked" denominator, via @c boss_roster — it
473# names two bosses the Memory table alone would miss. Cached per dir.
474_BOSS_FLAG_CACHE = {}
475
476
477def load_sdt_boss_flags(base_dir):
478 if base_dir not in _BOSS_FLAG_CACHE:
479 path = os.path.join(base_dir, "db_sdt", "boss_flags.json")
480 try:
481 with open(path, encoding="utf-8") as f:
482 _BOSS_FLAG_CACHE[base_dir] = json.load(f)
483 except (OSError, ValueError):
484 _BOSS_FLAG_CACHE[base_dir] = {}
485 return _BOSS_FLAG_CACHE[base_dir]
486
487
488##
489# @brief Load the Sekiro ending table (ending name → global event flag id).
490# @details The four adjacent flags came from four branches of the same ending save:
491# each terminal save sets exactly its own bit. Cached per dir with the other event
492# tables, because the generic combined timeline already knows how endings fork.
493_ENDING_CACHE = {}
494
495
496def load_sdt_endings(base_dir):
497 if base_dir not in _ENDING_CACHE:
498 path = os.path.join(base_dir, "db_sdt", "endings.json")
499 try:
500 with open(path, encoding="utf-8") as f:
501 _ENDING_CACHE[base_dir] = json.load(f)
502 except (OSError, ValueError):
503 _ENDING_CACHE[base_dir] = {}
504 return _ENDING_CACHE[base_dir]
505
506
507##
508# @brief Slot offset of the global event-flag category.
509# @details Sekiro serialises its flags exactly the way Dark Souls III does — this repo
510# has had that arithmetic since the DS3 bonfire work — and the only thing nobody
511# published was where the region lands in the FILE. It is here, at a fixed slot offset
512# like every other Sekiro field: ten 128-byte blocks of 1000 flags each, the id's
513# thousands digit picking the block and its last three digits addressing a bit inside
514# that block MSB-first.
515#
516# DERIVED, not ported. A save pair whose only act was killing Gyoubu Masataka Oniwa
517# leaves his flag `9301` as the single bit in the slot that goes 0 → 1, reads 0 in all
518# ten earlier saves on the ladder, and lights none of the other fourteen bosses — and it
519# is the only such candidate that also sits on the `0x500` grid the rest of the region
520# is spaced on. SoulSplitter, where the bit arithmetic comes from, reads process memory
521# and never opens a save, which is why this had to be measured.
522#
523# The PER-BLOCK packing is the part worth stating, because the obvious alternative is
524# wrong and looks right on every idol: read `9301` as the 9301st flag of one flat
525# `0x500` span and it lands 29 bytes early, on a byte that never moves in any save here.
526# An idol id's thousands digit is always 0, so idols cannot tell the two apart; a boss
527# can.
528SDT_FLAG_REGION = 52
529## @brief Bytes per category. Ten blocks, so ten thousand flags.
530SDT_FLAG_CATEGORY = 0x500
531## @brief Bytes per flag block — 1000 flags at one bit each, rounded up to the word.
532SDT_FLAG_BLOCK = 128
533## @brief Flags the global category holds. Ten blocks of a thousand.
534SDT_FLAG_GLOBAL_MAX = 10000
535
536
537##
538# @brief Each map's category index on the grid.
539# @details MEASURED, and the gaps are the finding rather than a hole: the maps are in
540# sorted order but do NOT sit in consecutive slots, because 6, 7, 10 and 12 belong to
541# maps with no Sculptor's Idol in them, which no table here can see. Guessing them
542# consecutive is exactly what a first pass did, and it read a save that had finished the
543# game as having never lit a lamp in Fountainhead Palace.
544#
545# Six of the nine are identified outright by how many idols the category reports — one
546# past its highest set bit — which is unique for that map. The two ties were broken
547# two-sidedly rather than by preference. `(11,2)` against `(13,0)`: the local run walked
548# the Ashina Reservoir in the prologue and has never entered the Abandoned Dungeon, and
549# reads 18 nonzero bytes in one against 0 in the other. `(17,0)` against `(25,0)`: on a
550# third-party checkpoint pack, one reads nine idols both before and after Owl while the
551# other reads zero then nine — and Fountainhead is the map you cannot reach until Owl is
552# dead.
553SDT_FLAG_MAPS = {
554 (10, 0): 2,
555 (11, 0): 3,
556 (11, 1): 4,
557 (11, 2): 5,
558 (13, 0): 8,
559 (15, 0): 9,
560 (17, 0): 11,
561 (20, 0): 13,
562 (25, 0): 14,
563}
564
565
566##
567# @brief How far a map's WORLD PICKUP category sits from the map's own category.
568# @details The `5xxxxxxx` item-lot family is not addressed differently — it is the same
569# arithmetic in a SECOND bank of categories, `SDT_FLAG_MAPS[(area, sub)] + 66`. That was
570# the last piece of the flag region, and it is measured rather than guessed.
571#
572# THE WINDOW. A 25-second pair (`Wolf_21-05-02` → `Wolf_21-05-27`) whose only acts were
573# picking up ONE Mibu Balloon of Wealth in Ashina Castle and lighting the Ashina Dojo
574# idol. The whole slot holds exactly two bits that go 0 → 1: the idol (`11110007`, which
575# this module already read) and one at `0x015E8A.7`.
576#
577# WHY THAT BIT CAN ONLY BE ONE ID. Eleven rows in `item_flags.json` are a Mibu Balloon of
578# Wealth. Nine die on the BIT INDEX alone — the bit is `7 - (n & 7)`, so bit 7 needs an id
579# whose last three digits are a multiple of eight. Of the two survivors, only `51110680`
580# (ashinacastle) puts its block start on the region's own `0x500` grid: k = 70 exactly,
581# against `SDT_FLAG_MAPS[(11, 1)] = 4`. `51110280` misses the grid by 406 bytes, which is
582# not a near miss to be argued over — it is arithmetic that does not close.
583#
584# SIXTY-SIX IS NOT A SEKIRO NUMBER, which is the part worth knowing. Dark Souls III's
585# pickup groups sit exactly 66 grid slots past their own map group — measured there six
586# times out of six before the other eight were predicted off it — so this is the same
587# engine convention in the next game, arrived at from the opposite direction. Neither
588# derivation used the other.
589#
590# CHECKED FOUR WAYS, none of them a score, and monotonicity is deliberately not among
591# them: the region is sparse, so "no flag ever clears" discriminates nothing (DS3's grid
592# work already paid for that lesson). What the checks are: the bit reads 0 in all 28
593# earlier saves on the ladder and 1 in the newest. The flags the seats report name items
594# the character actually holds — Ornamental Letter, Rotting Prisoner's Note, Treasure
595# Carp Scale, the Dragon's Blood Droplet picked up the day before. The four areas the run
596# has never entered (Ashina Depths, Sunken Valley, Senpou, Fountainhead) read ZERO at
597# their own seats. And a third-party FINISHED save reads 474 of 589 spread across all
598# nine areas, none of them empty — which is the shape a wrong seat cannot fake. (That
599# 589 was before the group-6 rows were refused, and two more went with @ref
600# load_sdt_lot_flags; the denominator is 581 now.)
601SDT_PICKUP_BANK = 66
602## @brief Top-level group of the item-lot family, the digit SoulSplitter switches on.
603SDT_PICKUP_GROUP = 5
604
605
606##
607# @brief Byte offset and bit of an event flag, or None where it cannot be placed.
608# @details The global category is `k=0` and holds ids 0..9999; a per-map flag is keyed by
609# the id's area and sub-area, and `k=1` is a second global-looking category that nothing
610# in either shipped table lands in, so it is left alone.
611#
612# THE `< SDT_FLAG_GLOBAL_MAX` GUARD IS LOAD-BEARING, not a tidy bounds check. SoulSplitter's
613# runtime addressing selects a top-level group on `(id // 10000000) % 10` before it ever
614# looks at the area, so the id families do not share one flat space — which is why the
615# group-5 branch is tested first here rather than being folded into the area lookup. Its
616# ids carry a map in the same digits, but a `50002001` reads area 0 and sub 0, and without
617# its own branch it would go down the global path and alias onto a real global flag in the
618# 0..9999 range. The guard is what keeps that honest for everything else that lands there.
619#
620# A group-5 id whose map has no seat in @ref SDT_FLAG_MAPS still returns None: 237 of the
621# 826 shipped pickups sit in families (areas 0, 1, 2, 4-8, 18, 19, 30-47) that no idol
622# names, so nothing here can place them, and a further six are `6xxxxxxx` — 583 placeable, 581 once @ref load_sdt_lot_flags
623# drops a misfiled row and a lot whose real flag has no seat. There are further populated
624# categories past the banks too (k=35..46 at least) and nothing published names a single flag in them.
625# @param fid The event flag id. @return @c (offset, bit) or None.
626def sdt_flag_offset(fid):
627 area, sub = (fid // 100000) % 100, (fid // 10000) % 10
628 if (fid // 10000000) % 10 == SDT_PICKUP_GROUP:
629 # The pickup bank, and the branch has to come FIRST: a `50002001` reads area 0
630 # and sub 0, which would otherwise fall down the global branch and alias onto
631 # somebody else's bit. A family whose map has no seat still returns None.
632 k = SDT_FLAG_MAPS.get((area, sub))
633 if k is None:
634 return None
635 k += SDT_PICKUP_BANK
636 elif area >= 90 or area + sub == 0:
637 if not 0 <= fid < SDT_FLAG_GLOBAL_MAX:
638 return None
639 k = 0
640 elif (fid // 10000000) % 10 > SDT_PICKUP_GROUP:
641 # A top-level group ABOVE the pickup bank has no seat here, and falling through
642 # to the map lookup is not a harmless guess — it is an alias. `item_flags.json`
643 # carries 115 `6xxxxxxx` rows, and six of them read an area this table seats:
644 # `61300000` (a Prayer Bead) lands byte-for-byte on the Underground Waterway
645 # idol's bit, so the pickup would report itself collected the moment that idol
646 # was lit. Measured on the `Wolf_22-01-15` -> `Wolf_22-01-35` pair, where one
647 # bit moved and two shipped ids both claimed it. Four of the six collide with an
648 # idol outright; the other two are the same arithmetic with nothing shipped on
649 # top of them yet. Nothing published says where group 6 lives, so it reads as
650 # unplaceable rather than as somebody else's flag.
651 return None
652 else:
653 k = SDT_FLAG_MAPS.get((area, sub))
654 if k is None:
655 return None
656 n = fid % 1000
657 block = (
658 SDT_FLAG_REGION + k * SDT_FLAG_CATEGORY + (fid // 1000) % 10 * SDT_FLAG_BLOCK
659 )
660 return block + (n >> 5) * 4 + 3 - ((n & 31) >> 3), 7 - (n & 7)
661
662
663##
664# @brief Which slots the game itself considers occupied: menu entry, one byte per slot.
665# @details Sekiro DOES publish an occupancy array after all, and this repo spent a long
666# time working around its absence by judging a slot from its own content (play time,
667# Steam id, any item). `mi5hmash/SL2Bonfire` carries the offset in its per-game profile
668# — `UserDataFileNumber` 10, `SlotsOccupancyOffset` 212 — and it is strictly better than
669# the content test, because content cannot tell a live character from a DELETED one.
670#
671# That is not theoretical: a third-party "100% complete" save reads three slots by
672# content and one by the array, and the two extra are ghosts the game does not show.
673# Exactly the DS2 case, where deleting a character clears the menu entry and leaves the
674# block intact — so Sekiro gets the same treatment DS2 already had.
675#
676# Degrades the same way DS2's does: an unreadable array, a byte that is not 0/1, or an
677# array claiming nothing is occupied all return None, which turns the filter OFF rather
678# than hiding real characters behind a moved offset.
679# @param data The full file bytes. @param entries The BND4 entries. @param decrypt The
680# game's own decrypt callable. @return A set of slot indices, or None.
681SDT_MENU_ENTRY = 10
682SDT_OCCUPANCY_OFF = 212
683
684
685def sdt_active_slots(data, entries, decrypt):
686 if len(entries) <= SDT_MENU_ENTRY:
687 return None
688 e = entries[SDT_MENU_ENTRY]
689 menu = decrypt(data[e.offset : e.offset + e.size])
690 if menu is None:
691 return None
692 active = set()
693 for i in range(SDT_SLOT_COUNT):
694 byte = u8(menu, SDT_OCCUPANCY_OFF + i)
695 if byte is None or byte > 1:
696 return None
697 if byte:
698 active.add(i)
699 return active or None
700
701
702##
703# @brief Load the miniboss table (area → [[entity id, name]]). Cached per dir.
704# @details The entity id is normally the defeat flag in Sekiro. Four map-state encounters
705# lose that local flag after their map changes, so @ref load_sdt_miniboss_fallbacks supplies
706# a permanent companion or ItemLot reward flag instead. A companion can name the key item
707# that gates the alternate map state, preventing a shared local flag from false-matching.
708# The roster itself remains generated by `tools/gamefiles.py roster --write` from the
709# health-bar events.
710_MINIBOSS_CACHE = {}
711
712
713def load_sdt_minibosses(base_dir):
714 if base_dir not in _MINIBOSS_CACHE:
715 path = os.path.join(base_dir, "db_sdt", "minibosses.json")
716 try:
717 with open(path, encoding="utf-8") as f:
718 _MINIBOSS_CACHE[base_dir] = json.load(f)
719 except (OSError, ValueError):
720 _MINIBOSS_CACHE[base_dir] = {}
721 return _MINIBOSS_CACHE[base_dir]
722
723
724## @brief Persistent reward flags for minibosses whose entity flags reset with map state.
725_MINIBOSS_FALLBACK_CACHE = {}
726
727
728def load_sdt_miniboss_fallbacks(base_dir):
729 if base_dir not in _MINIBOSS_FALLBACK_CACHE:
730 path = os.path.join(base_dir, "db_sdt", "miniboss_fallbacks.json")
731 try:
732 with open(path, encoding="utf-8") as f:
733 _MINIBOSS_FALLBACK_CACHE[base_dir] = json.load(f)
734 except (OSError, ValueError):
735 _MINIBOSS_FALLBACK_CACHE[base_dir] = {}
736 return _MINIBOSS_FALLBACK_CACHE[base_dir]
737
738
739## @brief Load the Sculptor's Idol table (area → [[flag id, name]]). Cached per dir.
740_IDOL_CACHE = {}
741
742
743def load_sdt_idols(base_dir):
744 if base_dir not in _IDOL_CACHE:
745 path = os.path.join(base_dir, "db_sdt", "idols.json")
746 try:
747 with open(path, encoding="utf-8") as f:
748 _IDOL_CACHE[base_dir] = json.load(f)
749 except (OSError, ValueError):
750 _IDOL_CACHE[base_dir] = {}
751 return _IDOL_CACHE[base_dir]
752
753
754## @brief Load the Sekiro item-lot flag table (family → [[flag id, name, map]]).
755# Cached per dir.
756_ITEM_FLAG_CACHE = {}
757
758
759def load_sdt_item_flags(base_dir):
760 if base_dir not in _ITEM_FLAG_CACHE:
761 path = os.path.join(base_dir, "db_sdt", "item_flags.json")
762 try:
763 with open(path, encoding="utf-8") as f:
764 _ITEM_FLAG_CACHE[base_dir] = json.load(f)
765 except (OSError, ValueError):
766 _ITEM_FLAG_CACHE[base_dir] = {}
767 return _ITEM_FLAG_CACHE[base_dir]
768
769
770##
771# @brief Load the corrections to that table: @c {"moved": {row id: flag}, "misfiled":
772# [[row id, name]]}. Cached per dir.
773# @details `item_flags.json` files a pickup under `50000000 + lot id`, and that is a
774# shortcut, not the game's rule. `ItemLotParam` gives every lot its own
775# `getItemFlagId`, and the lots that matter most break the shortcut: every prosthetic
776# part, esoteric text, Gourd Seed and world Prayer Bead sets a GLOBAL flag (`6500` for
777# the Shuriken Wheel). Read at the lot-shaped id, all of them sat in the "still out
778# there" list forever. `tools/gen_sdt_lot_flags.py` is the generator.
779#
780# Checked against the ladder rather than trusted: every moved flag in a read area first
781# sets in the save that picked the item up, none ever clears, and the moved Prayer Bead
782# flags count exactly the beads earned (four per necklace used, plus the ones held) on
783# every one of 60 saves, one short only once a shop bead is bought — the shop list is not
784# in the table. The Gourd Seeds close the same way against the gourd's charges.
785_LOT_FLAG_CACHE = {}
786
787
788def load_sdt_lot_flags(base_dir):
789 if base_dir not in _LOT_FLAG_CACHE:
790 path = os.path.join(base_dir, "db_sdt", "lot_flags.json")
791 try:
792 with open(path, encoding="utf-8") as f:
793 _LOT_FLAG_CACHE[base_dir] = json.load(f)
794 except (OSError, ValueError):
795 _LOT_FLAG_CACHE[base_dir] = {}
796 return _LOT_FLAG_CACHE[base_dir]
797
798
799##
800# @brief The area each seated pickup family belongs to, in `idols.json`'s own order.
801# @details Only the nine families with a seat are here, because only those can be read —
802# an area missing from the section means "not addressable", never "nothing found", and
803# the section's note says so. The area is always the ROW id's, even where the flag has
804# moved to a global one, because a global flag names no map. The Ashina Reservoir is its own map file `(11, 2)` while
805# `idols.json` files its idol under Ashina Castle, so it gets a row of its own rather
806# than being folded into a count that would then mean two different things.
807SDT_PICKUP_AREAS = {
808 (11, 0): "Ashina Outskirts",
809 (10, 0): "Hirata Estate",
810 (11, 1): "Ashina Castle",
811 (11, 2): "Ashina Reservoir",
812 (13, 0): "Abandoned Dungeon",
813 (20, 0): "Senpou Temple, Mt. Kongo",
814 (17, 0): "Sunken Valley",
815 (15, 0): "Ashina Depths",
816 (25, 0): "Fountainhead Palace",
817}
818
819
820## @brief The table's map slug, made readable. The slug is the game's own map name plus
821# the phase the item belongs to (`ashinacastle_invasion1`), which is worth keeping —
822# the same item lot appears in more than one phase — so it is spaced, not renamed.
823def sdt_place(where):
824 return (where or "").replace("_", " ").strip()
825
826
827##
828# @brief Every seated pickup, grouped by area in @ref SDT_PICKUP_AREAS order.
829# @details Rows in families with no seat are dropped here rather than in the reader, so
830# a total printed beside a count is the number of pickups that CAN be read in that area.
831# A misfiled row is dropped the same way — its id belongs to a lot that hands out
832# something else, so reading it would report that other item under this one's name.
833# @param base_dir Repo root. @return @c {(area, sub): [(flag to read, name, map)]}.
834def sdt_pickup_rows(base_dir):
835 fixes = load_sdt_lot_flags(base_dir)
836 moved = fixes.get("moved") or {}
837 misfiled = {(int(fid), name) for fid, name in fixes.get("misfiled") or []}
838 rows = {key: [] for key in SDT_PICKUP_AREAS}
839 for fid, name, where in load_sdt_item_flags(base_dir).get("item_pickup", []):
840 fid = int(fid)
841 key = ((fid // 100000) % 100, (fid // 10000) % 10)
842 # The area alone is not enough: six rows in a seated area are `6xxxxxxx`, a
843 # group with no seat of its own, so they would sit in the denominator forever
844 # unread. Ask the addressing, not the area.
845 if key not in rows or sdt_flag_offset(fid) is None or (fid, name) in misfiled:
846 continue
847 flag = int(moved.get(str(fid), fid))
848 if sdt_flag_offset(flag) is not None:
849 rows[key].append((flag, name, where))
850 return rows
851
852
853## @brief Read one event flag. None where the id cannot be placed or the slot is
854# too short — never a raw index, same as every other read in this module.
855def sdt_flag(buf, fid):
856 at = sdt_flag_offset(fid)
857 if at is None:
858 return None
859 byte = u8(buf, at[0])
860 return None if byte is None else (byte >> at[1]) & 1
861
862
863##
864# @brief Read Sekiro's event flags: boss defeats into @c ch["bosses"] as `flag`
865# evidence, and the Sculptor's Idols into @c ch["bonfire_areas"].
866# @details Must run AFTER @c attach_defeated_bosses, which refuses to do anything once
867# `bosses` exists — build the Memory floor first, then lay the flags on top. Getting
868# that order wrong silently drops the held-Memory kills, which is the trap DS1 already
869# fell into once.
870#
871# The boss half is what finally lets Sekiro report a kill it can PROVE rather than
872# infer: the Memory arithmetic counts tokens spent and the Memory items count tokens
873# held, but neither names the boss — a held Memory resolves as a bare "Memory". A flag
874# names it, and it never clears within a journey.
875#
876# Idols go into `bonfire_areas` rather than a field of their own, in DS3's
877# `[(area, count, [names], total, [missing])]` shape, so the render, the totals and the
878# combined timeline all take them with no new code — the same thing DS1's bonfire list
879# does. The rows follow `idols.json`'s own area order, which is why two of its keys map
880# into one flag category and one category feeds two keys: the areas are the game's, the
881# categories are the map files'.
882# @param ch A parsed character. @param buf The slot. @param base_dir Repo root.
883def sdt_attach_flags(ch, buf, base_dir):
884 bosses = ch.get("bosses") or {}
885 for name, fid in load_sdt_boss_flags(base_dir).items():
886 if sdt_flag(buf, fid):
887 bosses[name] = sorted(set(bosses.get(name, ())) | {"flag"})
888 if bosses:
889 ch["bosses"] = bosses
890 endings = [
891 name for name, fid in load_sdt_endings(base_dir).items() if sdt_flag(buf, fid)
892 ]
893 if endings:
894 ch["endings"] = endings
895 areas, any_lit = [], False
896 for area, idols in load_sdt_idols(base_dir).items():
897 named, missing = [], []
898 for fid, name in idols:
899 (named if sdt_flag(buf, int(fid)) else missing).append(name)
900 any_lit = any_lit or bool(named)
901 areas.append((area, len(named), named, len(idols), missing))
902 # Every area is kept, lit or not — an area reading 0/9 is the useful half. Only a
903 # character who has lit nothing anywhere gets no section, which is right: they are
904 # minutes from the opening and have not reached an idol.
905 if any_lit:
906 ch["bonfire_areas"] = areas
907 # Minibosses, in the BONFIRE shape (area, count, names, total, missing) rather than
908 # the pickup one, because a miniboss is a named kill and the reader wants to know
909 # WHICH — the same thing the boss section prints. Both lists want collapsing by name
910 # on the way out, because four of these really are "Shura Samurai" at four different
911 # entity ids and printing the name four times is faithful but unreadable.
912 minis, any_dead = [], False
913 fallbacks = load_sdt_miniboss_fallbacks(base_dir)
914 keys = {name for name, _qty in ch.get("key_items") or []}
915 for area, enemies in load_sdt_minibosses(base_dir).items():
916 dead, alive = [], []
917 for eid, name in enemies:
918 fallback = fallbacks.get(str(eid))
919 if isinstance(fallback, dict):
920 gate = fallback.get("key_item")
921 fallback = fallback.get("flag") if not gate or gate in keys else None
922 killed = sdt_flag(buf, int(eid)) or (
923 fallback is not None and sdt_flag(buf, int(fallback))
924 )
925 (dead if killed else alive).append(name)
926 any_dead = any_dead or bool(dead)
927 minis.append((area, len(dead), dead, len(enemies), alive))
928 if any_dead:
929 ch["minibosses"] = minis
930 # World pickups, in DS3's `(area, count, total, missing)` shape so the render, the
931 # JSON and the combined timeline take them with no new code. Only the nine seated
932 # families are counted — see @ref SDT_PICKUP_AREAS — and an item carries WHERE it is,
933 # because a bare "Pellet" is a tally and "Pellet — ashinacastle gate" is a to-do.
934 picks, any_found = [], False
935 for key, rows in sdt_pickup_rows(base_dir).items():
936 got, missing = [], []
937 for fid, name, where in rows:
938 label = f"{name} — {sdt_place(where)}" if where else name
939 (got if sdt_flag(buf, fid) else missing).append(label)
940 any_found = any_found or bool(got)
941 picks.append((SDT_PICKUP_AREAS[key], len(got), len(rows), missing))
942 if any_found:
943 ch["pickups"] = picks
sdt_weapon_cat(iid, prosthetics)
Definition sdt.py:182
sdt_twin(buf, off, alt)
Read a field the game stores twice, or None unless both copies agree.
Definition sdt.py:120