|
SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
|
Functions | |
| ds1_derived_stats (stats) | |
| DS1 base derived stats that are closed-form functions of attributes only. | |
| load_ds1_bonfires (base_dir) | |
| load_ds1_boss_flags (base_dir) | |
| ds1_attach_flags (ch, buf, base_dir, game) | |
Merge DS1 boss-defeat FLAGS into ch["bosses"] as flag evidence. | |
| load_ds1_known_flags (base_dir) | |
| load_ds1_world_events (base_dir) | |
| ds1_bonfires (buf, db) | |
| DS1 bonfires the character has found, grouped by area. | |
| ds1_augment (ch, data, entries, i, base_dir, dec) | |
| DS1-only augment: attach the bonfire list, which needs the db folder the parse function never sees. | |
| ds1_resolve (item_db, cat, iid) | |
| Resolve a DS1 item id to a display name, unwrapping any upgrade baked in. | |
| dsr_find_anchor (buf) | |
| Find the true DSR stat anchor. | |
| ds1_inventory (buf, item_db) | |
| Sort the DS1 inventory into categories. | |
| ds1_deaths (buf, game) | |
| Total deaths for a DS1 slot, or None when the struct isn't where expected. | |
| ds1_attach_playtime (ch, menu) | |
| Attach play time from DS1's load-screen roster block. | |
| ds1_character (buf, item_db, m, game, ng) | |
| Build the unified full-tier dict from a located DS1 stat anchor. | |
| dsr_parse (buf, item_db) | |
| Parse one DSR slot into the unified dict (full tier), or None if empty. | |
| ptde_find_anchor (buf) | |
| Find the PtDE stat anchor (full tier). | |
| ptde_parse (buf, item_db) | |
| Parse one PtDE slot (full tier). | |
Variables | |
| dict | _DS1_BONFIRE_CACHE = {} |
| Load the DS1 bonfire table (db_ds1/bonfires.json, NetBonfireDb id → [name, area]). | |
| dict | _DS1_BOSSFLAG_CACHE = {} |
| Load the DS1 boss-defeat flag table (db_ds1/boss_flags.json, canonical boss name → [region byte offset, uint32 mask]). | |
| dict | DS1_FLAG_BASE = {"dsr": 127721, "ptde": 127273} |
| Where the event-flag region starts in a decrypted DS1 slot, per game. | |
| DS1_FLAG_MAX_DENSITY | |
| Sanity gate on that base. | |
| DS1_FLAG_SPAN | |
| Sanity gate on that base. | |
| dict | _KNOWN_FLAG_CACHE = {} |
| Load the DS1 world-state flag table (category → [[offset, mask, name]]). | |
| dict | _WORLD_EVENT_CACHE = {} |
| Load the DS1 world-event table (family → [[offset, mask, Japanese name]]). | |
| DS1_BONFIRE_REC | |
| DS1 bonfire record: 20 bytes, id then state (the rest is unread flags). | |
| DS1_BONFIRE_STATE_D | |
| DS1 bonfire record: 20 bytes, id then state (the rest is unread flags). | |
| dict | DS1_BONFIRE_STATE |
| The state values a real record can hold, and what each means. | |
| int | DS1_BONFIRE_MIN_RUN = 5 |
| Shortest believable run, so a stray id in unrelated data cannot pass. | |
| DSR_MAGIC = bytes.fromhex("00FFFFFFFF000000000000000000000000FFFFFFFF") | |
| Anchor pattern that sits next to the DSR character block. | |
| DSR_SOULS_D | |
| DSR field distances from the anchor. | |
| DSR_HP_D | |
| DSR field distances from the anchor. | |
| DSR_STAM_D | |
| DSR field distances from the anchor. | |
| DSR_LEVEL_D | |
| DSR field distances from the anchor. | |
| DSR_CLASS_D | |
| DSR field distances from the anchor. | |
| DSR_HUM_D | |
| DSR field distances from the anchor. | |
| DSR_NG_D | |
| DSR_NAME_D | |
| int | DSR_GENDER_D = -237 |
| Gender (u8) distance from the anchor. | |
| dict | DS1_GENDER = {0: "Female", 1: "Male"} |
| DS1 gender enum. | |
| dict | DS1_DEATHS_OFF = {"ptde": 0x1F118, "dsr": 0x1F2D8} |
| Total deaths (u32), at a slot-absolute offset per release rather than a distance from the moving anchor — the counter lives in a fixed struct near the event-flag region, not in the character block. | |
| DS1_DEATHS_SENTINEL | |
| DS1_DEATHS_SENTINEL_D | |
| int | DS1_MENU_ENTRY = 10 |
| DS1's load-screen roster lives in BND4 entry 10, one fixed record per slot: name at +0 (UTF-16), soul level at +36, play time at +40 as a uint32 of SECONDS. | |
| DS1_MENU_LEVEL_D | |
| DS1_MENU_PLAYTIME_D | |
| int | DS1_EQUIP_BASE = 40 |
| DS1 derived values that are pure functions of one attribute, so they can be computed exactly rather than guessed. | |
| tuple | DS1_SLOT_BREAKS = (10, 12, 14, 16, 19, 23, 28, 34, 41, 50) |
| DSR_STAT_D | |
| DSR attribute distances from the anchor (uint8 each), in display order. | |
| dict | DS1_CLASS |
| DS1 class ids to names. | |
| dict | DS1_CAT |
| DS1 inventory slot type (top nibble) to category. | |
| DS1_INV_START | |
| Where the DS1 inventory scan begins, and the anchor that marks the first real slot. | |
| DS1_INV_ANCHOR | |
| Where the DS1 inventory scan begins, and the anchor that marks the first real slot. | |
| DS1_INV_END = bytes.fromhex("00000000FFFFFFFFFFFFFFFF") | |
| End-of-inventory marker. | |
| dict | DS1_INFUSION |
| DS1 weapon infusion paths, keyed by the hundreds digit of the id's upgrade suffix (id = base + path*100 + level). | |
Dark Souls 1 family: Remastered and Prepare to Die Edition.
| sl2.ds1.ds1_derived_stats | ( | stats | ) |
DS1 base derived stats that are closed-form functions of attributes only.
Only the two the equipment screen shows that need no gear: base Equip Load (40 + Endurance — fextralife's table is dead linear, END 10 -> 50.0, 99 -> 139.0) and attunement slots (DS1_SLOT_BREAKS). Stamina and Max HP are read from the save, so they are not recomputed; poise comes from armour alone and item discovery needs covenant/gear, so neither is derived.
| stats | The attribute dict. |
| sl2.ds1.load_ds1_bonfires | ( | base_dir | ) |
Definition at line 33 of file ds1.py.
Referenced by sl2.ds1.ds1_augment().
| sl2.ds1.ds1_attach_flags | ( | ch, | |
| buf, | |||
| base_dir, | |||
| game | |||
| ) |
Merge DS1 boss-defeat FLAGS into ch["bosses"] as flag evidence.
Unlike the held-soul floor this sees a boss whose soul was long since consumed. Guarded twice: the region base must be known for the game, and the region must actually read sparse (DS1_FLAG_MAX_DENSITY) — a moved region fails that and nothing is reported. Boss names are canonicalised to the boss_souls.json spelling in the db, so a flag kill and a soul kill dedup onto one boss.
Definition at line 87 of file ds1.py.
References sl2.ds1.load_ds1_known_flags(), and sl2.ds1.load_ds1_world_events().
Referenced by sl2.ds1.ds1_augment().
| sl2.ds1.load_ds1_known_flags | ( | base_dir | ) |
Definition at line 135 of file ds1.py.
Referenced by sl2.ds1.ds1_attach_flags().
| sl2.ds1.load_ds1_world_events | ( | base_dir | ) |
Definition at line 156 of file ds1.py.
Referenced by sl2.ds1.ds1_attach_flags().
| sl2.ds1.ds1_bonfires | ( | buf, | |
| db | |||
| ) |
DS1 bonfires the character has found, grouped by area.
The list's offset moves between saves, so it is located BY CONTENT: the longest run of consecutive 20-byte records whose id is a real bonfire and whose state is one of DS1_BONFIRE_STATE, with no id repeating. Shared by DSR and PtDE — the layout is identical, only the decryption differs.
[(area, count, [names])] in the DS3 shape so it renders the same way, or None when no believable run exists. Definition at line 196 of file ds1.py.
Referenced by sl2.ds1.ds1_augment().
| sl2.ds1.ds1_augment | ( | ch, | |
| data, | |||
| entries, | |||
| i, | |||
| base_dir, | |||
| dec | |||
| ) |
DS1-only augment: attach the bonfire list, which needs the db folder the parse function never sees.
Re-decrypts the slot rather than threading the buffer through, so the generic loop keeps its (ch, data, entries, i, base_dir) shape.
| dec | The game's decrypt callable (DSR is encrypted, PtDE is not). |
Definition at line 236 of file ds1.py.
References sl2.ds1.ds1_attach_flags(), sl2.ds1.ds1_attach_playtime(), sl2.ds1.ds1_bonfires(), and sl2.ds1.load_ds1_bonfires().
| sl2.ds1.ds1_resolve | ( | item_db, | |
| cat, | |||
| iid | |||
| ) |
Resolve a DS1 item id to a display name, unwrapping any upgrade baked in.
Weapons and armour store their reinforcement — and, for weapons, their infusion — inside the id as base+path*100+level, where base ends in 000. A direct hit is tried first; failing that, the base is looked up and a "+N" (with the infusion name for weapons) suffix is appended. Rings and goods do not upgrade, so they only ever match directly.
Definition at line 400 of file ds1.py.
Referenced by sl2.ds1.ds1_inventory().
| sl2.ds1.dsr_find_anchor | ( | buf | ) |
Find the true DSR stat anchor.
The magic pattern recurs inside runs of empty inventory slots, so a match is not enough — the right one is where the whole stat block also reads as plausible (level in range, every attribute 0..99). This is why a wrong anchor never slips through on an all-items save.
| buf | The decrypted slot data. |
Definition at line 426 of file ds1.py.
Referenced by sl2.ds1.dsr_parse().
| sl2.ds1.ds1_inventory | ( | buf, | |
| item_db | |||
| ) |
Sort the DS1 inventory into categories.
Shared by DSR and PtDE.
(buckets, unknown_count). Definition at line 446 of file ds1.py.
References sl2.ds1.ds1_resolve().
Referenced by sl2.ds1.ds1_character().
| sl2.ds1.ds1_deaths | ( | buf, | |
| game | |||
| ) |
Total deaths for a DS1 slot, or None when the struct isn't where expected.
Guarded by the sentinel that follows the counter in both releases: if the uint32 at +4 isn't 0xFFFFFFFF the struct has moved and the field is dropped rather than read from the wrong place.
| buf | The decrypted slot. |
| game | "dsr" or "ptde". |
Definition at line 481 of file ds1.py.
Referenced by sl2.ds1.ds1_character().
| sl2.ds1.ds1_attach_playtime | ( | ch, | |
| menu | |||
| ) |
Attach play time from DS1's load-screen roster block.
The roster record is found by the character's own name and accepted only when the level stored beside it matches the level already parsed from the slot, so a renamed/duplicate name or a shifted block turns the field off instead of attaching another character's clock.
| ch | The parsed character (read for name/level, written for play_time). |
| menu | The decrypted menu block, or None. |
Definition at line 498 of file ds1.py.
Referenced by sl2.ds1.ds1_augment().
| sl2.ds1.ds1_character | ( | buf, | |
| item_db, | |||
| m, | |||
| game, | |||
| ng | |||
| ) |
Build the unified full-tier dict from a located DS1 stat anchor.
Shared by DSR and PtDE: the two games carry the same stat block — same fields at the same signed distances from the same anchor point (proven by reading a real PtDE save byte-for-byte against the DSR distances). Only the way the anchor is found differs, and NG+ is DSR-file-specific, so the caller passes it (PtDE has no calibrated NG+ field and passes None).
| m | The stat anchor (a DSR-equivalent anchor position). |
| ng | New Game+ count, or None to omit the field. |
Definition at line 519 of file ds1.py.
References sl2.ds1.ds1_deaths(), and sl2.ds1.ds1_inventory().
Referenced by sl2.ds1.dsr_parse(), and sl2.ds1.ptde_parse().
| sl2.ds1.dsr_parse | ( | buf, | |
| item_db | |||
| ) |
Parse one DSR slot into the unified dict (full tier), or None if empty.
Definition at line 547 of file ds1.py.
References sl2.ds1.ds1_character(), and sl2.ds1.dsr_find_anchor().
| sl2.ds1.ptde_find_anchor | ( | buf | ) |
Find the PtDE stat anchor (full tier).
PtDE has no DSR_MAGIC to key on, but its stat block is laid out exactly like DSR's around the character name. So the name is the anchor: for each position that decodes as a valid name, treat it as DSR's name field, back out the equivalent anchor, and accept it only if the whole stat block there also reads sane (level in range, every attribute 0..99). Requiring a valid name and a valid stat block is what stops a false match inside the repeating inventory runs of an all-items save — the real block sits before the inventory, so the first such match from the top is the character.
Definition at line 565 of file ds1.py.
Referenced by sl2.ds1.ptde_parse().
| sl2.ds1.ptde_parse | ( | buf, | |
| item_db | |||
| ) |
Parse one PtDE slot (full tier).
Unencrypted DS1. Same stat layout as DSR (see ds1_character), found via the name anchor. NG+ is not calibrated for PtDE, so it is omitted.
Definition at line 587 of file ds1.py.
References sl2.ds1.ds1_character(), and sl2.ds1.ptde_find_anchor().
|
protected |
|
protected |
| dict sl2.ds1.DS1_FLAG_BASE = {"dsr": 127721, "ptde": 127273} |
Where the event-flag region starts in a decrypted DS1 slot, per game.
The published DS1 flag addressing (group base + area*0x500 + section*128 + number/8, MSB-first mask) gives offsets INSIDE the region; the region's own position is not published, so it was searched for. In the DSR mule — an NG+2 character with all 43 bonfires — exactly ONE offset in the whole 393216-byte slot has all twelve boss flags and both Bells of Awakening set, and both PtDE saves independently agree on their own single value, so the base is a per-game constant rather than a per-save search.
| sl2.ds1.DS1_FLAG_MAX_DENSITY |
Sanity gate on that base.
A real flag region is overwhelmingly zero — it measures ~0.006 set bits at the true base against ~0.32 for ordinary save data — so anything denser than this means the region moved and the feature turns itself off rather than reporting bosses off the wrong bytes.
| sl2.ds1.DS1_FLAG_SPAN |
Sanity gate on that base.
A real flag region is overwhelmingly zero — it measures ~0.006 set bits at the true base against ~0.32 for ordinary save data — so anything denser than this means the region moved and the feature turns itself off rather than reporting bosses off the wrong bytes.
|
protected |
Load the DS1 world-state flag table (category → [[offset, mask, name]]).
Generated by tools/gen_ds1_known_flags.py, whose id→(offset, mask) function reproduces all twelve hand-checked entries of boss_flags.json exactly — which is why these 44 are trusted without each being verified on its own. Cached per dir.
|
protected |
Load the DS1 world-event table (family → [[offset, mask, Japanese name]]).
Generated by tools/gen_ds1_world_events.py from the committed .emevd plus FromSoft's own .emeld event names. Every row is derived and none is verified against a save here — it decodes and it discriminates, and that is all that is claimed. Read the generator before believing a family name: Levers reads 0 on a finished run and Boss-fight flags counts flags rather than bosses. Cached per dir.
| sl2.ds1.DS1_BONFIRE_REC |
| sl2.ds1.DS1_BONFIRE_STATE_D |
| dict sl2.ds1.DS1_BONFIRE_STATE |
The state values a real record can hold, and what each means.
Anything else ends the walk, which is what keeps a misaligned start from inventing bonfires.
| int sl2.ds1.DS1_BONFIRE_MIN_RUN = 5 |
| sl2.ds1.DSR_MAGIC = bytes.fromhex("00FFFFFFFF000000000000000000000000FFFFFFFF") |
| int sl2.ds1.DSR_GENDER_D = -237 |
Gender (u8) distance from the anchor.
Two independent sources agree, which is what makes this shippable without a differential save: alfizari's DSR editor puts Gender at magic-237, and tarvitz/dsfp (a PtDE parser) has a boolean male field 34 bytes past the name — the same byte, since the name sits at magic-271. Both call 1 Male, so DS1's polarity is the OPPOSITE of DS2's (where 1 is Female). Cross-read on a real save: dsfp's frame and ours both report male=1 for the same character.
| dict sl2.ds1.DS1_GENDER = {0: "Female", 1: "Male"} |
| dict sl2.ds1.DS1_DEATHS_OFF = {"ptde": 0x1F118, "dsr": 0x1F2D8} |
Total deaths (u32), at a slot-absolute offset per release rather than a distance from the moving anchor — the counter lives in a fixed struct near the event-flag region, not in the character block.
PtDE's offset is dsfp's (0x1F128 in its frame, which is 16 bytes ahead of ours), verified on two real saves: an all-items mule reads 459 and a real playthrough reads 39. DSR shifts this struct by the same 448 bytes its event-flag region moves (see DS1_FLAG_BASE), and the neighbouring fields confirm it: both releases read [deaths][0xFFFFFFFF][~1.5M][2048] in that order. DS1_DEATHS_SENTINEL is checked before the value is used, so a moved struct omits the field instead of printing whatever is there.
| int sl2.ds1.DS1_MENU_ENTRY = 10 |
DS1's load-screen roster lives in BND4 entry 10, one fixed record per slot: name at +0 (UTF-16), soul level at +36, play time at +40 as a uint32 of SECONDS.
dsfp documents the 0x170 record stride, which its own play-time constant confirms (its file-absolute index minus the record base is exactly this +40). The block's start differs between the releases, so the record is located by the character's own name and only accepted when the level at +36 matches the level parsed from the slot — a self-consistency gate, the same trick DS3's equip slots use. Verified on three saves: 25:16:39 at level 95 (DSR), 19:25:30 at 81 (PtDE), 151h on a mule.
| int sl2.ds1.DS1_EQUIP_BASE = 40 |
DS1 derived values that are pure functions of one attribute, so they can be computed exactly rather than guessed.
Equip Load is 40 + Endurance (fextralife's table: END 10 -> 50.0, 40 -> 80.0, 99 -> 139.0, dead linear with base 40). Attunement slots are the documented breakpoints, 10 slots max at 50. Stamina and HP are NOT computed — DS1 stores both in the save, so they are read. Poise is armour- only and everything else is gear-scaled, so nothing else is derived.
| tuple sl2.ds1.DS1_SLOT_BREAKS = (10, 12, 14, 16, 19, 23, 28, 34, 41, 50) |
| sl2.ds1.DSR_STAT_D |
DSR attribute distances from the anchor (uint8 each), in display order.
| dict sl2.ds1.DS1_CLASS |
| dict sl2.ds1.DS1_CAT |
| sl2.ds1.DS1_INV_START |
| sl2.ds1.DS1_INV_ANCHOR |
| sl2.ds1.DS1_INV_END = bytes.fromhex("00000000FFFFFFFFFFFFFFFF") |
| dict sl2.ds1.DS1_INFUSION |
DS1 weapon infusion paths, keyed by the hundreds digit of the id's upgrade suffix (id = base + path*100 + level).
Path 0 is plain reinforcement.