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

Functions

 sdt_twin (buf, off, alt)
 Read a field the game stores twice, or None unless both copies agree.
 
 sdt_weapon_cat (iid, prosthetics)
 

Variables

int SDT_SLOT_COUNT = 10
 How many character slots the game has.
 
int SDT_STEAM_OFF = 0x33E54
 Slot fields, at fixed offsets into the decrypted (plaintext) slot payload.
 
int SDT_NG_OFF = 0x33F34
 
int SDT_PLAYTIME_OFF = 0x33F80
 
int SDT_ATTACK_OFF = 0x3449C
 
int SDT_SEN_OFF = 0x344D0
 
 SDT_HP_OFF
 Max HP and max Posture, each stored TWICE, and neither at the offset the published source names.
 
 SDT_HP_ALT
 Max HP and max Posture, each stored TWICE, and neither at the offset the published source names.
 
 SDT_POSTURE_OFF
 
 SDT_POSTURE_ALT
 
int SDT_VITALITY_OFF = 0x34498
 Vitality, the second of Sekiro's two upgrade tracks, at the word immediately before Attack Power.
 
int SDT_VITALITY_MAX = 20
 
int SDT_GOURD_OFF = 0x34562
 The Healing Gourd's CHARGE COUNT — a byte, and Sekiro's third spent-token counter — at 0x34562.
 
int SDT_GOURD_MAX = 10
 
int SDT_ATTACK_BASE = 1
 Attack power on a character who has consumed no Memory.
 
 SDT_ATTACK_MAX
 Ceilings that make a field a field rather than noise: a value past either means the read landed somewhere it should not have, and the field is dropped.
 
 SDT_NG_MAX
 Ceilings that make a field a field rather than noise: a value past either means the read landed somewhere it should not have, and the field is dropped.
 
tuple SDT_LISTS
 One inventory-style list: where it starts and how long it runs.
 
int SDT_RECORD = 16
 
dict SDT_CAT = {0x8: "weapons", 0x9: "armors", 0xB: "goods"}
 Item type, from the top nibble of the record's HANDLE.
 
int SDT_ID_MASK = 0x00FFFFFF
 The item id proper is the low 24 bits; the rest is the type code.
 
int SDT_ARTS_MAX = 9999
 Category refinement for the weapons table, applied at load time.
 

Detailed Description

Sekiro: Shadows Die Twice.

The odd one out in three ways, and each of them makes the read simpler rather than
harder. The save is NOT encrypted (plaintext BND4 entries behind a plain MD5, like
PtDE and Elden Ring), the slot offsets do NOT move between patches (so there is no
content scan and no level identity to check), and reinforcement is NOT baked into an
item id — a prosthetic tool's upgrade tier is its own id, so a straight lookup names
"Lazulite Shuriken" with no arithmetic.

What it does not have is a character name — Sekiro's profiles are unnamed by design —
or attributes. Its numbers are Attack Power, max HP and max Posture, and the two upgrade
levels behind them: Vitality and the Healing Gourd. Neither of the last two is in any
published source; both were pinned here by differentials, and both are spent-token
counters — the item that raised them is consumed, so the level is the only record that
it was ever held.

Offsets from uberhalit/SimpleSekiroSavegameHelper (the container) and
alfizari/Sekiro-Save-Editor (the slot fields), every one of them verified against a real
S0000.sl2 — and two of that editor's labels corrected against a differential, because
they point at the current value rather than the maximum.

Function Documentation

◆ sdt_twin()

sl2.sdt.sdt_twin (   buf,
  off,
  alt 
)

Read a field the game stores twice, or None unless both copies agree.

Parameters
bufThe slot payload.
offThe field.
altIts second copy.

Definition at line 120 of file sdt.py.

Referenced by sl2.sdt.sdt_weapon_cat().

Here is the caller graph for this function:

◆ sdt_weapon_cat()

sl2.sdt.sdt_weapon_cat (   iid,
  prosthetics 
)

Definition at line 182 of file sdt.py.

References sl2.sdt.sdt_twin(), and sl2.sdt.sdt_weapon_cat().

Referenced by sl2.sdt.sdt_weapon_cat().

Here is the call graph for this function:
Here is the caller graph for this function:

Variable Documentation

◆ SDT_SLOT_COUNT

int sl2.sdt.SDT_SLOT_COUNT = 10

How many character slots the game has.

Entry 10 is the settings/profile block and entry 11 (present on the current patch, absent in the published layout) is reserved and reads all zeros.

Definition at line 31 of file sdt.py.

◆ SDT_STEAM_OFF

int sl2.sdt.SDT_STEAM_OFF = 0x33E54

Slot fields, at fixed offsets into the decrypted (plaintext) slot payload.

These do not move between patches — Sekiro has no equivalent of the DS3 stat block that drifts — so they are read directly rather than searched for. The Steam id is NOT printed anywhere; it is read only as the occupancy test, because an unused slot is all zeros and a used one carries its owner's id. That was checked both ways: the id matches the save's own folder name, and the nine unused slots (and a second, characterless save file) read zero.

Definition at line 42 of file sdt.py.

◆ SDT_NG_OFF

int sl2.sdt.SDT_NG_OFF = 0x33F34

Definition at line 43 of file sdt.py.

◆ SDT_PLAYTIME_OFF

int sl2.sdt.SDT_PLAYTIME_OFF = 0x33F80

Definition at line 44 of file sdt.py.

◆ SDT_ATTACK_OFF

int sl2.sdt.SDT_ATTACK_OFF = 0x3449C

Definition at line 45 of file sdt.py.

◆ SDT_SEN_OFF

int sl2.sdt.SDT_SEN_OFF = 0x344D0

Definition at line 46 of file sdt.py.

◆ SDT_HP_OFF

sl2.sdt.SDT_HP_OFF

Max HP and max Posture, each stored TWICE, and neither at the offset the published source names.

Both live in the same four-word shape — [0][current][max][max] — and alfizari's editor labels the CURRENT field of each as the maximum. A real differential settles it: across a 42-minute window the word at 0x3446C moved 32 → 160 while 0x34470 and 0x34474 both held at 320. A value that moves while its neighbours do not is the current one; the pair that holds is the maximum. Posture sits in the identical shape one group along (its zero word at 0x34484), so its maximum is the same two fields over — which also explains why all three of its words read 120: posture is a pool that depletes, and an undamaged character is at full.

Each is read only where BOTH copies agree and the value is nonzero. That is the same self-consistency gate the DS3 equipment slots use, and here it costs nothing: the game writes both, so a disagreement means the read landed somewhere it should not have.

Definition at line 64 of file sdt.py.

◆ SDT_HP_ALT

sl2.sdt.SDT_HP_ALT

Max HP and max Posture, each stored TWICE, and neither at the offset the published source names.

Both live in the same four-word shape — [0][current][max][max] — and alfizari's editor labels the CURRENT field of each as the maximum. A real differential settles it: across a 42-minute window the word at 0x3446C moved 32 → 160 while 0x34470 and 0x34474 both held at 320. A value that moves while its neighbours do not is the current one; the pair that holds is the maximum. Posture sits in the identical shape one group along (its zero word at 0x34484), so its maximum is the same two fields over — which also explains why all three of its words read 120: posture is a pool that depletes, and an undamaged character is at full.

Each is read only where BOTH copies agree and the value is nonzero. That is the same self-consistency gate the DS3 equipment slots use, and here it costs nothing: the game writes both, so a disagreement means the read landed somewhere it should not have.

Definition at line 64 of file sdt.py.

◆ SDT_POSTURE_OFF

sl2.sdt.SDT_POSTURE_OFF

Definition at line 65 of file sdt.py.

◆ SDT_POSTURE_ALT

sl2.sdt.SDT_POSTURE_ALT

Definition at line 65 of file sdt.py.

◆ SDT_VITALITY_OFF

int sl2.sdt.SDT_VITALITY_OFF = 0x34498

Vitality, the second of Sekiro's two upgrade tracks, at the word immediately before Attack Power.

In no published source at all — it was pinned by the differential the old blocker asked for. A 21-second window in which the character used four Prayer Beads (the First Prayer Necklace) moved Max HP 320 → 400 and Max Posture 120 → 150, and in the whole player struct exactly ONE other word moved: this one, 1 → 2. Every earlier save on the same ladder reads 1 with no necklace used, and a characterless save reads 0, so the field is not a coincidence of that one window.

The stored value IS the number the status screen shows — no base to subtract, unlike SDT_ATTACK_BASE, because a fresh character reads 1 and one necklace makes it 2. The ceiling is the in-game cap; past it the read landed somewhere it should not have.

Definition at line 81 of file sdt.py.

◆ SDT_VITALITY_MAX

int sl2.sdt.SDT_VITALITY_MAX = 20

Definition at line 82 of file sdt.py.

◆ SDT_GOURD_OFF

int sl2.sdt.SDT_GOURD_OFF = 0x34562

The Healing Gourd's CHARGE COUNT — a byte, and Sekiro's third spent-token counter — at 0x34562.

In no published source either, and pinned the same way Vitality was. A four-minute window in which one Gourd Seed was handed to Emma moved exactly one byte in the whole player struct: this one, 7 → 8. The other five words that moved in the same window are play time, two timers and a pair that only tracks with play time, none of them a small monotone count.

Two independent checks across the whole 45-save ladder say what the byte MEANS, and they disagree with the obvious first reading of "seeds used":

  • Conservation. A seed is consumed the moment Emma takes it, so this byte + the Gourd Seeds still in the inventory must never fall. It never does, over 45 saves, and on each save where a held seed disappears the byte takes exactly the +1 the inventory lost. A wrong offset cannot stay conserved against a list parsed independently of it.
  • It is the capacity, not the tally. The Healing Gourd is an ordinary inventory row whose quantity is its remaining charges, and that quantity never exceeds this byte and repeatedly equals it — 4 on all eight saves at 4, 5 at 5, 7 at 7. Were the byte a count of seeds with charges some fixed amount above it, a gourd refilled at every idol would have to read higher than this somewhere in 45 saves. It never does.

So the byte IS the number of charges, it starts at 1 (a characterless save and every unused slot read 0; the earliest save on the ladder reads 1 with a one-charge gourd), and one seed buys one charge — which makes value - 1 the seeds consumed, cumulative across journeys like Attack Power and Vitality. The ceiling is the game's own: the maxNum of EquipParamGoods row 3000, read out of this machine's install.

Definition at line 113 of file sdt.py.

◆ SDT_GOURD_MAX

int sl2.sdt.SDT_GOURD_MAX = 10

Definition at line 114 of file sdt.py.

◆ SDT_ATTACK_BASE

int sl2.sdt.SDT_ATTACK_BASE = 1

Attack power on a character who has consumed no Memory.

Read off a real save that is minutes from the opening — no Memory held, no gourd, one key item — which is what makes the base a measurement rather than an assumption. It matters because sdt_memories_spent subtracts it.

Definition at line 129 of file sdt.py.

◆ SDT_ATTACK_MAX

sl2.sdt.SDT_ATTACK_MAX

Ceilings that make a field a field rather than noise: a value past either means the read landed somewhere it should not have, and the field is dropped.

Attack power was 98 here and that was one too low — a real journey-9 save reads exactly 99, so the tool was silently dropping Attack Power, and with it the Memories line, on precisely the characters that have the most of it. Raised on that evidence. The gate exists to reject a read in the thousands, not to tell 98 from 99.

Note the Memories arithmetic UNDERCOUNTS at the cap, because Attack Power stops rising while the kills do not. It is a floor, which is what the line already says.

Definition at line 142 of file sdt.py.

◆ SDT_NG_MAX

sl2.sdt.SDT_NG_MAX

Ceilings that make a field a field rather than noise: a value past either means the read landed somewhere it should not have, and the field is dropped.

Attack power was 98 here and that was one too low — a real journey-9 save reads exactly 99, so the tool was silently dropping Attack Power, and with it the Memories line, on precisely the characters that have the most of it. Raised on that evidence. The gate exists to reject a read in the thousands, not to tell 98 from 99.

Note the Memories arithmetic UNDERCOUNTS at the cap, because Attack Power stops rising while the kills do not. It is a floor, which is what the line already says.

Definition at line 142 of file sdt.py.

◆ SDT_LISTS

tuple sl2.sdt.SDT_LISTS
Initial value:
1= (
2 ("inv", 0x8F70C, 0x7000),
3 ("key", 0x9670C, 0x2000),
4 ("storage", 0x987A0, 0x9000),
5 ("storage", 0xA1958, 0x4000),
6)

One inventory-style list: where it starts and how long it runs.

Records are 16 bytes, [u32 handle][u32 item id][u32 quantity][u32 index]. key items get their own region in Sekiro (every other game needs them filtered out of a bucket), and the storage box is kept apart because an item in the box is owned but not carried — a distinction the report can only make if the read makes it too.

Definition at line 150 of file sdt.py.

◆ SDT_RECORD

int sl2.sdt.SDT_RECORD = 16

Definition at line 158 of file sdt.py.

◆ SDT_CAT

dict sl2.sdt.SDT_CAT = {0x8: "weapons", 0x9: "armors", 0xB: "goods"}

Item type, from the top nibble of the record's HANDLE.

Scoped by construction, the same property Elden Ring's id nibble gives: an armour handle cannot resolve to a weapon of the same number. 0x0 is an empty record.

Definition at line 164 of file sdt.py.

◆ SDT_ID_MASK

int sl2.sdt.SDT_ID_MASK = 0x00FFFFFF

The item id proper is the low 24 bits; the rest is the type code.

Definition at line 168 of file sdt.py.

◆ SDT_ARTS_MAX

int sl2.sdt.SDT_ARTS_MAX = 9999

Category refinement for the weapons table, applied at load time.

Sekiro keeps combat arts, prosthetic tools and a pile of engine-internal rows in one param table, so one heading for all of them would be useless. The split is by id block, read off the table sorted rather than guessed: the prosthetic tools are exactly the ids in prosthetics.json (the 7xxxx range, one id per upgrade tier), the combat arts are the five-digit ids below them, and everything above is the skill tree plus the "Virtual Weapon:" / "Upgrade Menu:" internals.

Definition at line 179 of file sdt.py.