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 Skill points are a spendable currency, not a consumable, so the report puts
247# them beside Attack Power and Vitality instead of in with the sugars and the gourds.
248SDT_SKILL_POINT_ID = 1200
249
250
251## @brief The db_sdt files that carry real English names, one per item type.
252SDT_DB_FILES = ("weapons", "armors", "goods")
253
254
255##
256# @brief Load the Sekiro id tables, type-scoped: @c {category: {id: name}}.
257# @details A fourth id scheme, and the simplest of the four: decimal id keys, one file
258# per type, no category collisions to design around because the handle nibble already
259# says which table to look in.
260#
261# The @c *_devnames.json files are loaded into their own table and never merged. They
262# hold Paramdex's machine-translated Japanese dev strings for the ids with no English
263# name ("ID monitoring item 1", "Right-handed sword_style: None"), and most of them are
264# engine internals a player never sees — merging them would put debug rows in the
265# inventory under the same heading as real items. Anything that resolves only there is
266# reported as an internal entry instead, so it is neither hidden nor dressed up.
267# @param db_dir Folder holding the tables.
268# @return @c {"names": {cat: {id: name}}, "dev": {cat: {id: name}}, "prosthetics": set}.
269def load_sdt_db(db_dir):
270 def table(stem):
271 try:
272 with open(os.path.join(db_dir, stem + ".json"), encoding="utf-8") as f:
273 return {int(k): v for k, v in json.load(f).items()}
274 except (OSError, ValueError):
275 return {}
276
277 names = {cat: table(cat) for cat in SDT_DB_FILES}
278 dev = {cat: table(cat + "_devnames") for cat in SDT_DB_FILES}
279 return {"names": names, "dev": dev, "prosthetics": set(table("prosthetics"))}
280
281
282##
283# @brief Resolve one item id to @c (name, category, internal).
284# @details The type nibble decides which table is consulted, so a lookup cannot cross
285# types. A hit in the named table gives the render category; a hit only in the dev-name
286# table is flagged @c internal, which is how the caller knows to count it rather than
287# print it. A miss in both is left to the caller to count as unknown.
288# @param cat The type from the handle nibble (weapons/armors/goods).
289# @param iid The masked item id.
290# @param db The bundle from @ref load_sdt_db.
291def sdt_resolve(cat, iid, db):
292 name = db["names"].get(cat, {}).get(iid)
293 if name is not None:
294 if cat == "weapons":
295 return name, sdt_weapon_cat(iid, db["prosthetics"]), False
296 if cat == "goods":
297 return name, sdt_goods_cat(iid), False
298 return name, cat, False
299 name = db["dev"].get(cat, {}).get(iid)
300 return (name, "internal", True) if name is not None else (None, None, False)
301
302
303##
304# @brief Walk Sekiro's four item lists.
305# @details Each region is a flat array of 16-byte records and the game leaves the tail
306# zeroed, so an empty record (handle 0) is skipped rather than treated as the end —
307# the count that matters is which records carry a known type nibble. That gate is what
308# keeps a mis-located region from inventing items: random bytes clear it three times in
309# sixteen, and a zeroed tail never clears it at all.
310# @param buf The slot payload. @param db The bundle from @ref load_sdt_db.
311# @return A generator of @c (which list, item id, name, category, quantity, internal).
312def sdt_items(buf, db):
313 for which, start, length in SDT_LISTS:
314 for off in range(start, start + length, SDT_RECORD):
315 handle = u32(buf, off)
316 if not handle:
317 continue
318 cat = SDT_CAT.get((handle >> 28) & 0xF)
319 if cat is None:
320 continue
321 iid = u32(buf, off + 4)
322 if iid is None:
323 continue
324 iid &= SDT_ID_MASK
325 name, render_cat, internal = sdt_resolve(cat, iid, db)
326 qty = u32(buf, off + 8)
327 yield which, iid, name, render_cat, qty, internal
328
329
330##
331# @brief How many Memories this character has consumed, or None.
332# @details Sekiro's boss tokens are Memories, and consuming one at an idol raises
333# Attack Power by exactly one — so a stored Attack Power is a COUNT of the Memories
334# already spent, which is the one thing no other game in this repo can recover. Every
335# other boss floor here goes blind the moment the token is used; this one does not.
336#
337# Two honest limits, both stated where it is rendered. The base is 1, not 0
338# (@ref SDT_ATTACK_BASE), and Attack Power carries across New Game+ while the Memories
339# do not, so past journey 0 the number counts every lap rather than this one.
340# @param attack The stored attack power, or None. @return The count, or None.
341def sdt_memories_spent(attack):
342 if attack is None or not SDT_ATTACK_BASE <= attack <= SDT_ATTACK_MAX:
343 return None
344 return attack - SDT_ATTACK_BASE
345
346
347##
348# @brief Parse one Sekiro slot into the unified character dict.
349# @details No name and no attributes: both are absent from the game, not missing from
350# the tool, and the report says so rather than leaving a blank. An unused slot is all
351# zeros, which is how a populated one is told apart — the game publishes no occupancy
352# array, so the test is the slot's own content.
353#
354# Max HP and max Posture are read from the second of each field's two copies, NOT from
355# the offset alfizari names — see @ref sdt_twin for the differential that settles it.
356#
357# **Spirit emblems** is still deliberately NOT read. The documented @c uint16 at
358# @c 0x3459A reads 15 on a character who owns no Spirit Emblem item and has no prosthetic
359# at all, and stayed 15 across a window in which the inventory grew by four items — so it
360# is the carry CAP (which starts at 15), not the count. It needs a save either side of
361# actually spending emblems, and a wrong number is worse than none.
362# @param buf The slot payload. @param db The bundle from @ref load_sdt_db.
363# @return A unified character dict, or None if the slot is empty.
364def sdt_parse(buf, db):
365 inv, key_items, memories, unknown, internal = {}, [], [], 0, 0
366 suppressed, skill_points = 0, None
367 for which, iid, name, cat, qty, is_internal in sdt_items(buf, db):
368 if name is None:
369 unknown += 1
370 continue
371 if is_internal:
372 internal += 1
373 continue
374 if sdt_suppressed(name):
375 suppressed += 1
376 continue
377 # A skill point is spendable currency, so it rides in the header beside the
378 # other two upgrade tracks rather than in with the sugars. Only a CARRIED one
379 # is promoted — a copy sitting in the box is genuinely in the box, and the
380 # storage list would otherwise lose it with nothing saying so.
381 if iid == SDT_SKILL_POINT_ID and which != "storage":
382 skill_points = (skill_points or 0) + (qty or 0)
383 continue
384 row = (name, qty)
385 # The box wins over the category: a key item sitting in storage is in
386 # storage, and saying otherwise would report it as carried.
387 if which == "storage":
388 inv.setdefault("storage", []).append(row)
389 elif which == "key" or cat == "key":
390 key_items.append(row)
391 else:
392 inv.setdefault(cat, []).append(row)
393 if cat == "memories":
394 memories.append(row)
395
396 play_time = u32(buf, SDT_PLAYTIME_OFF)
397 steam_id = u64(buf, SDT_STEAM_OFF)
398 if not any((inv, key_items, play_time, steam_id)):
399 return None
400
401 attack = u32(buf, SDT_ATTACK_OFF)
402 if attack is not None and attack > SDT_ATTACK_MAX:
403 attack = None
404 ng = u8(buf, SDT_NG_OFF)
405 vitality = u32(buf, SDT_VITALITY_OFF)
406 if vitality is not None and not 1 <= vitality <= SDT_VITALITY_MAX:
407 vitality = None
408 gourd = u8(buf, SDT_GOURD_OFF)
409 if gourd is not None and not 1 <= gourd <= SDT_GOURD_MAX:
410 gourd = None
411 ch = {
412 "tier": "full",
413 "game": "sdt",
414 # Sekiro profiles carry no name — the game never asks for one — so the slot
415 # is labelled the same way an empty Elden Ring roster entry is. What that
416 # means is said once, in SDT_NOTE, rather than inside every heading and
417 # every chart box that has to carry the name.
418 "name": "(unnamed)",
419 "klass": None,
420 "stats": {},
421 "level": None,
422 "soul_memory": None,
423 "humanity": None,
424 "stamina": None,
425 "hp": sdt_twin(buf, SDT_HP_OFF, SDT_HP_ALT),
426 "posture": sdt_twin(buf, SDT_POSTURE_OFF, SDT_POSTURE_ALT),
427 "ng_plus": ng if ng is not None and ng <= SDT_NG_MAX else None,
428 "play_time": play_time,
429 "souls": u32(buf, SDT_SEN_OFF),
430 "attack": attack,
431 "vitality": vitality,
432 "gourd": gourd,
433 "skill_points": skill_points,
434 "boss_souls": memories,
435 "key_items": key_items,
436 "inv": inv,
437 "unknown_count": unknown,
438 "internal_count": internal,
439 "suppressed_count": suppressed,
440 }
441 spent = sdt_memories_spent(attack)
442 if spent is not None:
443 ch["memories"] = {
444 "spent": spent,
445 "held": len(memories),
446 "cumulative": bool(ch["ng_plus"]),
447 }
448 return ch
449
450
451##
452# @brief Load the Sekiro boss-defeat flag table (boss name → event flag id).
453# @details Also the source of the "of N tracked" denominator, via @c boss_roster — it
454# names two bosses the Memory table alone would miss. Cached per dir.
455_BOSS_FLAG_CACHE = {}
456
457
458def load_sdt_boss_flags(base_dir):
459 if base_dir not in _BOSS_FLAG_CACHE:
460 path = os.path.join(base_dir, "db_sdt", "boss_flags.json")
461 try:
462 with open(path, encoding="utf-8") as f:
463 _BOSS_FLAG_CACHE[base_dir] = json.load(f)
464 except (OSError, ValueError):
465 _BOSS_FLAG_CACHE[base_dir] = {}
466 return _BOSS_FLAG_CACHE[base_dir]
467
468
469##
470# @brief Slot offset of the global event-flag category.
471# @details Sekiro serialises its flags exactly the way Dark Souls III does — this repo
472# has had that arithmetic since the DS3 bonfire work — and the only thing nobody
473# published was where the region lands in the FILE. It is here, at a fixed slot offset
474# like every other Sekiro field: ten 128-byte blocks of 1000 flags each, the id's
475# thousands digit picking the block and its last three digits addressing a bit inside
476# that block MSB-first.
477#
478# DERIVED, not ported. A save pair whose only act was killing Gyoubu Masataka Oniwa
479# leaves his flag `9301` as the single bit in the slot that goes 0 → 1, reads 0 in all
480# ten earlier saves on the ladder, and lights none of the other fourteen bosses — and it
481# is the only such candidate that also sits on the `0x500` grid the rest of the region
482# is spaced on. SoulSplitter, where the bit arithmetic comes from, reads process memory
483# and never opens a save, which is why this had to be measured.
484#
485# The PER-BLOCK packing is the part worth stating, because the obvious alternative is
486# wrong and looks right on every idol: read `9301` as the 9301st flag of one flat
487# `0x500` span and it lands 29 bytes early, on a byte that never moves in any save here.
488# An idol id's thousands digit is always 0, so idols cannot tell the two apart; a boss
489# can.
490SDT_FLAG_REGION = 52
491## @brief Bytes per category. Ten blocks, so ten thousand flags.
492SDT_FLAG_CATEGORY = 0x500
493## @brief Bytes per flag block — 1000 flags at one bit each, rounded up to the word.
494SDT_FLAG_BLOCK = 128
495## @brief Flags the global category holds. Ten blocks of a thousand.
496SDT_FLAG_GLOBAL_MAX = 10000
497
498
499##
500# @brief Each map's category index on the grid.
501# @details MEASURED, and the gaps are the finding rather than a hole: the maps are in
502# sorted order but do NOT sit in consecutive slots, because 6, 7, 10 and 12 belong to
503# maps with no Sculptor's Idol in them, which no table here can see. Guessing them
504# consecutive is exactly what a first pass did, and it read a save that had finished the
505# game as having never lit a lamp in Fountainhead Palace.
506#
507# Six of the nine are identified outright by how many idols the category reports — one
508# past its highest set bit — which is unique for that map. The two ties were broken
509# two-sidedly rather than by preference. `(11,2)` against `(13,0)`: the local run walked
510# the Ashina Reservoir in the prologue and has never entered the Abandoned Dungeon, and
511# reads 18 nonzero bytes in one against 0 in the other. `(17,0)` against `(25,0)`: on a
512# third-party checkpoint pack, one reads nine idols both before and after Owl while the
513# other reads zero then nine — and Fountainhead is the map you cannot reach until Owl is
514# dead.
515SDT_FLAG_MAPS = {
516 (10, 0): 2,
517 (11, 0): 3,
518 (11, 1): 4,
519 (11, 2): 5,
520 (13, 0): 8,
521 (15, 0): 9,
522 (17, 0): 11,
523 (20, 0): 13,
524 (25, 0): 14,
525}
526
527
528##
529# @brief How far a map's WORLD PICKUP category sits from the map's own category.
530# @details The `5xxxxxxx` item-lot family is not addressed differently — it is the same
531# arithmetic in a SECOND bank of categories, `SDT_FLAG_MAPS[(area, sub)] + 66`. That was
532# the last piece of the flag region, and it is measured rather than guessed.
533#
534# THE WINDOW. A 25-second pair (`Wolf_21-05-02` → `Wolf_21-05-27`) whose only acts were
535# picking up ONE Mibu Balloon of Wealth in Ashina Castle and lighting the Ashina Dojo
536# idol. The whole slot holds exactly two bits that go 0 → 1: the idol (`11110007`, which
537# this module already read) and one at `0x015E8A.7`.
538#
539# WHY THAT BIT CAN ONLY BE ONE ID. Eleven rows in `item_flags.json` are a Mibu Balloon of
540# Wealth. Nine die on the BIT INDEX alone — the bit is `7 - (n & 7)`, so bit 7 needs an id
541# whose last three digits are a multiple of eight. Of the two survivors, only `51110680`
542# (ashinacastle) puts its block start on the region's own `0x500` grid: k = 70 exactly,
543# against `SDT_FLAG_MAPS[(11, 1)] = 4`. `51110280` misses the grid by 406 bytes, which is
544# not a near miss to be argued over — it is arithmetic that does not close.
545#
546# SIXTY-SIX IS NOT A SEKIRO NUMBER, which is the part worth knowing. Dark Souls III's
547# pickup groups sit exactly 66 grid slots past their own map group — measured there six
548# times out of six before the other eight were predicted off it — so this is the same
549# engine convention in the next game, arrived at from the opposite direction. Neither
550# derivation used the other.
551#
552# CHECKED FOUR WAYS, none of them a score, and monotonicity is deliberately not among
553# them: the region is sparse, so "no flag ever clears" discriminates nothing (DS3's grid
554# work already paid for that lesson). What the checks are: the bit reads 0 in all 28
555# earlier saves on the ladder and 1 in the newest. The flags the seats report name items
556# the character actually holds — Ornamental Letter, Rotting Prisoner's Note, Treasure
557# Carp Scale, the Dragon's Blood Droplet picked up the day before. The four areas the run
558# has never entered (Ashina Depths, Sunken Valley, Senpou, Fountainhead) read ZERO at
559# their own seats. And a third-party FINISHED save reads 474 of 589 spread across all
560# nine areas, none of them empty — which is the shape a wrong seat cannot fake. (That
561# 589 was before the group-6 rows were refused; the denominator is 583 now.)
562SDT_PICKUP_BANK = 66
563## @brief Top-level group of the item-lot family, the digit SoulSplitter switches on.
564SDT_PICKUP_GROUP = 5
565
566
567##
568# @brief Byte offset and bit of an event flag, or None where it cannot be placed.
569# @details The global category is `k=0` and holds ids 0..9999; a per-map flag is keyed by
570# the id's area and sub-area, and `k=1` is a second global-looking category that nothing
571# in either shipped table lands in, so it is left alone.
572#
573# THE `< SDT_FLAG_GLOBAL_MAX` GUARD IS LOAD-BEARING, not a tidy bounds check. SoulSplitter's
574# runtime addressing selects a top-level group on `(id // 10000000) % 10` before it ever
575# looks at the area, so the id families do not share one flat space — which is why the
576# group-5 branch is tested first here rather than being folded into the area lookup. Its
577# ids carry a map in the same digits, but a `50002001` reads area 0 and sub 0, and without
578# its own branch it would go down the global path and alias onto a real global flag in the
579# 0..9999 range. The guard is what keeps that honest for everything else that lands there.
580#
581# A group-5 id whose map has no seat in @ref SDT_FLAG_MAPS still returns None: 237 of the
582# 826 shipped pickups sit in families (areas 0, 1, 2, 4-8, 18, 19, 30-47) that no idol
583# names, so nothing here can place them, and a further six are `6xxxxxxx` — 583 readable. There are further populated categories past the
584# banks too (k=35..46 at least) and nothing published names a single flag in them.
585# @param fid The event flag id. @return @c (offset, bit) or None.
586def sdt_flag_offset(fid):
587 area, sub = (fid // 100000) % 100, (fid // 10000) % 10
588 if (fid // 10000000) % 10 == SDT_PICKUP_GROUP:
589 # The pickup bank, and the branch has to come FIRST: a `50002001` reads area 0
590 # and sub 0, which would otherwise fall down the global branch and alias onto
591 # somebody else's bit. A family whose map has no seat still returns None.
592 k = SDT_FLAG_MAPS.get((area, sub))
593 if k is None:
594 return None
595 k += SDT_PICKUP_BANK
596 elif area >= 90 or area + sub == 0:
597 if not 0 <= fid < SDT_FLAG_GLOBAL_MAX:
598 return None
599 k = 0
600 elif (fid // 10000000) % 10 > SDT_PICKUP_GROUP:
601 # A top-level group ABOVE the pickup bank has no seat here, and falling through
602 # to the map lookup is not a harmless guess — it is an alias. `item_flags.json`
603 # carries 115 `6xxxxxxx` rows, and six of them read an area this table seats:
604 # `61300000` (a Prayer Bead) lands byte-for-byte on the Underground Waterway
605 # idol's bit, so the pickup would report itself collected the moment that idol
606 # was lit. Measured on the `Wolf_22-01-15` -> `Wolf_22-01-35` pair, where one
607 # bit moved and two shipped ids both claimed it. Four of the six collide with an
608 # idol outright; the other two are the same arithmetic with nothing shipped on
609 # top of them yet. Nothing published says where group 6 lives, so it reads as
610 # unplaceable rather than as somebody else's flag.
611 return None
612 else:
613 k = SDT_FLAG_MAPS.get((area, sub))
614 if k is None:
615 return None
616 n = fid % 1000
617 block = (
618 SDT_FLAG_REGION + k * SDT_FLAG_CATEGORY + (fid // 1000) % 10 * SDT_FLAG_BLOCK
619 )
620 return block + (n >> 5) * 4 + 3 - ((n & 31) >> 3), 7 - (n & 7)
621
622
623##
624# @brief Which slots the game itself considers occupied: menu entry, one byte per slot.
625# @details Sekiro DOES publish an occupancy array after all, and this repo spent a long
626# time working around its absence by judging a slot from its own content (play time,
627# Steam id, any item). `mi5hmash/SL2Bonfire` carries the offset in its per-game profile
628# — `UserDataFileNumber` 10, `SlotsOccupancyOffset` 212 — and it is strictly better than
629# the content test, because content cannot tell a live character from a DELETED one.
630#
631# That is not theoretical: a third-party "100% complete" save reads three slots by
632# content and one by the array, and the two extra are ghosts the game does not show.
633# Exactly the DS2 case, where deleting a character clears the menu entry and leaves the
634# block intact — so Sekiro gets the same treatment DS2 already had.
635#
636# Degrades the same way DS2's does: an unreadable array, a byte that is not 0/1, or an
637# array claiming nothing is occupied all return None, which turns the filter OFF rather
638# than hiding real characters behind a moved offset.
639# @param data The full file bytes. @param entries The BND4 entries. @param decrypt The
640# game's own decrypt callable. @return A set of slot indices, or None.
641SDT_MENU_ENTRY = 10
642SDT_OCCUPANCY_OFF = 212
643
644
645def sdt_active_slots(data, entries, decrypt):
646 if len(entries) <= SDT_MENU_ENTRY:
647 return None
648 e = entries[SDT_MENU_ENTRY]
649 menu = decrypt(data[e.offset : e.offset + e.size])
650 if menu is None:
651 return None
652 active = set()
653 for i in range(SDT_SLOT_COUNT):
654 byte = u8(menu, SDT_OCCUPANCY_OFF + i)
655 if byte is None or byte > 1:
656 return None
657 if byte:
658 active.add(i)
659 return active or None
660
661
662##
663# @brief Load the miniboss table (area → [[entity id, name]]). Cached per dir.
664# @details The entity id IS the defeat flag in Sekiro — not the separate per-map block
665# Dark Souls III uses — which is why this table needs no flag column. Measured, not
666# assumed; the change log carries the four checks. The table itself is generated by
667# `tools/gamefiles.py roster --write` from the game's own event scripts, so the names are
668# the ones the game prints over the health bar.
669_MINIBOSS_CACHE = {}
670
671
672def load_sdt_minibosses(base_dir):
673 if base_dir not in _MINIBOSS_CACHE:
674 path = os.path.join(base_dir, "db_sdt", "minibosses.json")
675 try:
676 with open(path, encoding="utf-8") as f:
677 _MINIBOSS_CACHE[base_dir] = json.load(f)
678 except (OSError, ValueError):
679 _MINIBOSS_CACHE[base_dir] = {}
680 return _MINIBOSS_CACHE[base_dir]
681
682
683## @brief Load the Sculptor's Idol table (area → [[flag id, name]]). Cached per dir.
684_IDOL_CACHE = {}
685
686
687def load_sdt_idols(base_dir):
688 if base_dir not in _IDOL_CACHE:
689 path = os.path.join(base_dir, "db_sdt", "idols.json")
690 try:
691 with open(path, encoding="utf-8") as f:
692 _IDOL_CACHE[base_dir] = json.load(f)
693 except (OSError, ValueError):
694 _IDOL_CACHE[base_dir] = {}
695 return _IDOL_CACHE[base_dir]
696
697
698## @brief Load the Sekiro item-lot flag table (family → [[flag id, name, map]]).
699# Cached per dir.
700_ITEM_FLAG_CACHE = {}
701
702
703def load_sdt_item_flags(base_dir):
704 if base_dir not in _ITEM_FLAG_CACHE:
705 path = os.path.join(base_dir, "db_sdt", "item_flags.json")
706 try:
707 with open(path, encoding="utf-8") as f:
708 _ITEM_FLAG_CACHE[base_dir] = json.load(f)
709 except (OSError, ValueError):
710 _ITEM_FLAG_CACHE[base_dir] = {}
711 return _ITEM_FLAG_CACHE[base_dir]
712
713
714##
715# @brief The area each seated pickup family belongs to, in `idols.json`'s own order.
716# @details Only the nine families with a seat are here, because only those can be read —
717# an area missing from the section means "not addressable", never "nothing found", and
718# the section's note says so. The Ashina Reservoir is its own map file `(11, 2)` while
719# `idols.json` files its idol under Ashina Castle, so it gets a row of its own rather
720# than being folded into a count that would then mean two different things.
721SDT_PICKUP_AREAS = {
722 (11, 0): "Ashina Outskirts",
723 (10, 0): "Hirata Estate",
724 (11, 1): "Ashina Castle",
725 (11, 2): "Ashina Reservoir",
726 (13, 0): "Abandoned Dungeon",
727 (20, 0): "Senpou Temple, Mt. Kongo",
728 (17, 0): "Sunken Valley",
729 (15, 0): "Ashina Depths",
730 (25, 0): "Fountainhead Palace",
731}
732
733
734## @brief The table's map slug, made readable. The slug is the game's own map name plus
735# the phase the item belongs to (`ashinacastle_invasion1`), which is worth keeping —
736# the same item lot appears in more than one phase — so it is spaced, not renamed.
737def sdt_place(where):
738 return (where or "").replace("_", " ").strip()
739
740
741##
742# @brief Every seated pickup, grouped by area in @ref SDT_PICKUP_AREAS order.
743# @details Rows in families with no seat are dropped here rather than in the reader, so
744# a total printed beside a count is the number of pickups that CAN be read in that area.
745# @param base_dir Repo root. @return @c {(area, sub): [(flag id, name, map)]}.
746def sdt_pickup_rows(base_dir):
747 rows = {key: [] for key in SDT_PICKUP_AREAS}
748 for fid, name, where in load_sdt_item_flags(base_dir).get("item_pickup", []):
749 fid = int(fid)
750 key = ((fid // 100000) % 100, (fid // 10000) % 10)
751 # The area alone is not enough: six rows in a seated area are `6xxxxxxx`, a
752 # group with no seat of its own, so they would sit in the denominator forever
753 # unread. Ask the addressing, not the area.
754 if key in rows and sdt_flag_offset(fid) is not None:
755 rows[key].append((fid, name, where))
756 return rows
757
758
759## @brief Read one event flag. None where the id cannot be placed or the slot is
760# too short — never a raw index, same as every other read in this module.
761def sdt_flag(buf, fid):
762 at = sdt_flag_offset(fid)
763 if at is None:
764 return None
765 byte = u8(buf, at[0])
766 return None if byte is None else (byte >> at[1]) & 1
767
768
769##
770# @brief Read Sekiro's event flags: boss defeats into @c ch["bosses"] as `flag`
771# evidence, and the Sculptor's Idols into @c ch["bonfire_areas"].
772# @details Must run AFTER @c attach_defeated_bosses, which refuses to do anything once
773# `bosses` exists — build the Memory floor first, then lay the flags on top. Getting
774# that order wrong silently drops the held-Memory kills, which is the trap DS1 already
775# fell into once.
776#
777# The boss half is what finally lets Sekiro report a kill it can PROVE rather than
778# infer: the Memory arithmetic counts tokens spent and the Memory items count tokens
779# held, but neither names the boss — a held Memory resolves as a bare "Memory". A flag
780# names it, and it never clears within a journey.
781#
782# Idols go into `bonfire_areas` rather than a field of their own, in DS3's
783# `[(area, count, [names], total, [missing])]` shape, so the render, the totals and the
784# combined timeline all take them with no new code — the same thing DS1's bonfire list
785# does. The rows follow `idols.json`'s own area order, which is why two of its keys map
786# into one flag category and one category feeds two keys: the areas are the game's, the
787# categories are the map files'.
788# @param ch A parsed character. @param buf The slot. @param base_dir Repo root.
789def sdt_attach_flags(ch, buf, base_dir):
790 bosses = ch.get("bosses") or {}
791 for name, fid in load_sdt_boss_flags(base_dir).items():
792 if sdt_flag(buf, fid):
793 bosses[name] = sorted(set(bosses.get(name, ())) | {"flag"})
794 if bosses:
795 ch["bosses"] = bosses
796 areas, any_lit = [], False
797 for area, idols in load_sdt_idols(base_dir).items():
798 named, missing = [], []
799 for fid, name in idols:
800 (named if sdt_flag(buf, int(fid)) else missing).append(name)
801 any_lit = any_lit or bool(named)
802 areas.append((area, len(named), named, len(idols), missing))
803 # Every area is kept, lit or not — an area reading 0/9 is the useful half. Only a
804 # character who has lit nothing anywhere gets no section, which is right: they are
805 # minutes from the opening and have not reached an idol.
806 if any_lit:
807 ch["bonfire_areas"] = areas
808 # Minibosses, in the BONFIRE shape (area, count, names, total, missing) rather than
809 # the pickup one, because a miniboss is a named kill and the reader wants to know
810 # WHICH — the same thing the boss section prints. Both lists want collapsing by name
811 # on the way out, because four of these really are "Shura Samurai" at four different
812 # entity ids and printing the name four times is faithful but unreadable.
813 minis, any_dead = [], False
814 for area, enemies in load_sdt_minibosses(base_dir).items():
815 dead, alive = [], []
816 for eid, name in enemies:
817 (dead if sdt_flag(buf, int(eid)) else alive).append(name)
818 any_dead = any_dead or bool(dead)
819 minis.append((area, len(dead), dead, len(enemies), alive))
820 if any_dead:
821 ch["minibosses"] = minis
822 # World pickups, in DS3's `(area, count, total, missing)` shape so the render, the
823 # JSON and the combined timeline take them with no new code. Only the nine seated
824 # families are counted — see @ref SDT_PICKUP_AREAS — and an item carries WHERE it is,
825 # because a bare "Pellet" is a tally and "Pellet — ashinacastle gate" is a to-do.
826 picks, any_found = [], False
827 for key, rows in sdt_pickup_rows(base_dir).items():
828 got, missing = [], []
829 for fid, name, where in rows:
830 label = f"{name} — {sdt_place(where)}" if where else name
831 (got if sdt_flag(buf, fid) else missing).append(label)
832 any_found = any_found or bool(got)
833 picks.append((SDT_PICKUP_AREAS[key], len(got), len(rows), missing))
834 if any_found:
835 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