|
SL2 Analyzer
Read a FromSoftware .sl2 save and report what is in it
|
Functions | |
| load_ds3_ring_effects (base_dir) | |
| ds3_ring_level (name) | |
| Split an equipped ring name into its table key and reinforcement level. | |
| ds3_attach_ring_effects (ch, base_dir) | |
| Attach each equipped ring's documented effect, and the subset of those effects that is a structured modifier to a derived stat. | |
| ds3_ring_bonuses (ring_mods) | |
| Sum the worn rings' structured modifiers. | |
| ds3_derived_stats (stats, ring_mods=None) | |
| DS3 base derived stats that are closed-form functions of attributes only, plus the worn rings' documented bonuses to those same three values. | |
| load_ds3_bonfires (base_dir) | |
| load_ds3_boss_flags (base_dir) | |
| load_ds3_boss_victory (base_dir) | |
| load_ds3_lord_cinders (base_dir) | |
| load_ds3_questlines (base_dir) | |
| load_ds3_pickups (base_dir) | |
| load_ds3_enemies (base_dir) | |
| load_ds3_npcs (base_dir) | |
| load_ds3_covenants (base_dir) | |
| load_ds3_endings (base_dir) | |
| ds3_goods_cat (iid) | |
Refine a DS3 goods id to its finer category (see DS3_GOODS_RANGES). | |
| ds3_item_cat (iid, cat) | |
| Refine a scanned DS3 id's category. | |
| scan_inventory (buf, iddb) | |
| Find inventory by scanning for known item ids in fixed-size records. | |
| ds3_find_stats (buf) | |
| Locate the DS3 stat block by content, or None if none validates. | |
| ds3_embered (buf, v) | |
| DS3 embered state for a slot, or None. | |
| ds3_covenant (buf, v) | |
| DS3 covenant name for a slot, or None. | |
| ds3_resolve_weapon (iddb, iid) | |
| Resolve an equipped DS3 weapon id to a name, unwrapping the baked "+N". | |
| ds3_resolve_estus (iid) | |
Resolve a DS3 goods id to an Estus flask name with its +N, or None. | |
| ds3_gaitem_map (buf) | |
| Map every GaItem handle to its item id by walking the GaItem array. | |
| ds3_equipped_weapons (buf, iddb, v) | |
| Equipped weapons (up to three per hand) from EquipGameData. | |
| ds3_equipped_armor (buf, iddb, v) | |
| Equipped armour (the four protection slots) from EquipGameData. | |
| ds3_equipped_rings (buf, iddb, v) | |
| Equipped rings (up to four) from EquipGameData. | |
| ds3_equipped_ammo (buf, iddb, v) | |
| Equipped ammunition (the arrow/bolt quiver slots) from EquipGameData. | |
| ds3_parse (buf, iddb, name) | |
| Parse one DS3 slot into the unified dict (full tier where stats validate). | |
| ds3_playtime (menu_data, i) | |
| ds3_event_flag_base (buf) | |
| Locate the DS3 event-flag region base in a decrypted slot, or None. | |
| ds3_attach_flags (ch, buf, base, base_dir) | |
Read DS3 bonfires + boss-defeat flags off the event-flag region and attach them to ch: bonfire_areas as [(area, count, [names])] — every lit bonfire named from load_ds3_bonfires — and merge flag boss evidence into ch["bosses"] (deduping with any soul/gate evidence already there, from both the per-map and the group-6 victory tables), cinders as the Lords of Cinder whose ashes are on the throne, and endings as the endings this character has reached (cumulative, so an NG+ save can hold several). | |
| ds3_journey (buf, base) | |
Variables | |
| tuple | DS3_SLOT_BREAKS = (10, 14, 18, 24, 30, 40, 50, 60, 80, 99) |
| DS3 attunement-slot breakpoints (fextralife Attunement table): the nth entry is the ATN needed for the nth spell slot. | |
| dict | _DS3_RING_CACHE = {} |
Load db_ds3/ring_effects.json: ring name → {"effect": [lines], "mods": {level: {stat: value}}}. | |
| dict | _DS3_BONFIRE_CACHE = {} |
Load the DS3 bonfire table (db_ds3/bonfires.json): area → list of [distance, bit, name], one entry per bonfire. | |
| dict | _DS3_BOSS_CACHE = {} |
Load the DS3 boss-defeat flag table (db_ds3/boss_flags.json): boss name → [distance, bit] of its single "boss dead" event flag (the 13xxxx8xx per-map flag). | |
| dict | _DS3_VICTORY_CACHE = {} |
Load the DS3 boss-victory flag table (db_ds3/boss_victory.json): boss name → [distance, bit] of its 63xx "boss victory" event flag, a SECOND independent kill signal that survives the soul being consumed. | |
| dict | _DS3_CINDER_CACHE = {} |
Load the DS3 Lords-of-Cinder table (db_ds3/lord_cinders.json): lord name → [distance, bit] of the flag set when that lord's Cinders are placed on the Firelink throne. | |
| dict | _DS3_QUEST_CACHE = {} |
Load the DS3 NPC-questline table (db_ds3/questlines.json): NPC/source → list of [distance, bit, reward] — one per reward that NPC hands out, each a "you received this" event flag in the common group 50006. | |
| dict | _DS3_PICKUP_CACHE = {} |
Load the DS3 world item-pickup table (db_ds3/item_pickups.json): area → list of [distance, bit, item] — one per one-off item lying in that area, each a "you picked this up" event flag in the world groups 533xx–540xx. | |
| dict | _DS3_ENEMY_CACHE = {} |
Load the one-time-enemy table (db_ds3/enemies.json): area → list of [distance, bit, enemy type] — one per enemy that does not respawn and whose death DS3 therefore has to remember. | |
| dict | _DS3_NPC_CACHE = {} |
| Load the DS3 NPC-state table (db_ds3/npcs.json): family → [[distance, bit, label]] — NPCs killed, NPCs turned hostile, questline states. | |
| dict | _DS3_COV_CACHE = {} |
| Load the DS3 covenant table (db_ds3/covenants.json, {covenant: [[region distance, bit, what it proves]]}). | |
| dict | _DS3_ENDING_CACHE = {} |
Load the DS3 endings table (db_ds3/endings.json): ending name → [distance, bit]. | |
| DS3_RECORD | |
| DS3 held-item record size and the offset of the quantity within it. | |
| DS3_QTY_OFF | |
| DS3 held-item record size and the offset of the quantity within it. | |
| DS3_STAT_D | |
| DS3 stat block as signed distances from the Vigor field (the anchor). | |
| DS3_HP_D | |
| DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme. | |
| DS3_FP_D | |
| DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme. | |
| DS3_STAM_D | |
| DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme. | |
| DS3_LEVEL_D | |
| DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme. | |
| DS3_SOULS_D | |
| DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme. | |
| int | DS3_EMBER_D = 188 |
| DS3 embered flag: a lone uint8 at +188 in the stat-mirror struct behind the stat anchor — 1 = embered, 0 = hollow. | |
| int | DS3_COVENANT_D = 3944 |
| DS3 covenant: a uint32 EQUIP HANDLE at +3944 from the stat anchor, not a small enum — the game models the covenant as a worn accessory, so the field holds 0xA00027xx and the covenant's own item id is the low 28 bits (Rosaria's Fingers 0xA0002760 -> 10080). | |
| dict | DS3_COVENANT |
| int | DS3_LEVEL_BASE = 89 |
| DS3's soul-level identity: level == (sum of all nine attributes) - 89. | |
| int | SCAN_MIN_RUN = 3 |
| Shortest run of consecutive records that counts as real inventory. | |
| int | DS3_MAX_RUN_GAP = 48 |
| Largest gap (bytes) a run may bridge. | |
| int | DS3_GOODS_ID_BASE = 0x40000000 |
| DS3 goods ids carry a type prefix (0x40000000); the real id is what the game's own EquipParamGoods table uses, and it blocks out cleanly by kind. | |
| tuple | DS3_GOODS_RANGES |
| dict | DS3_GOODS_OVERRIDE |
| The two flask upgrades that sit in the key-item block but are materials. | |
| DS3_AMMO_LO | |
| Where ammunition lives in the weapon id space. | |
| DS3_AMMO_HI | |
| Where ammunition lives in the weapon id space. | |
| int | DS3_EQUIP_D = 664 |
| EquipGameData sits a fixed 664 bytes past the stat (Vigor) anchor — invariant across saves whose anchor itself moves (56124..75156 on the Joy ladder), so it is read at a fixed anchor-relative offset, not searched for. | |
| DS3_ARMOR_SLOTS | |
| Armour sub-offsets inside EquipGameData (base +0x20..+0x2C), in the game's own head-to-toe order. | |
| tuple | DS3_RING_SLOTS = (0x34, 0x38, 0x3C, 0x40) |
| The four ring sub-offsets inside EquipGameData (base +0x34..+0x40). | |
| tuple | DS3_AMMO_SLOTS = (0x08, 0x0C, 0x10, 0x14) |
The ammo sub-offsets (Arrow 1 / Bolt 1 / Arrow 2 / Bolt 2) at base +0x08..+0x14 — GaItem handles like weapons/armour (arrows and bolts share the db's bolts category). | |
| DS3_WEAPON_SLOTS | |
| The six weapon sub-offsets, in the game's own two-hand-by-slot order. | |
| int | DS3_FISTS = 110000 |
| Item id of the default bare fist (an empty weapon slot reads this, not a null handle), so a slot resolving to it is skipped as "unarmed". | |
| int | DS3_REINF_MAX = 10 |
| Max reinforcement level, a sanity bound on the id-baked "+N". | |
| int | DS3_GOODS_TYPE = 0x40000000 |
| The two Estus flasks' base goods ids, and the reinforcement ceiling. | |
| tuple | DS3_ESTUS = ((150, "Estus Flask"), (190, "Ashen Estus Flask")) |
| int | DS3_ESTUS_MAX = 10 |
| int | DS3_RING_ID_MASK = 0x0FFFFFFF |
| A ring's equip handle encodes its own id: the low 28 bits are shared and the type nibble is 0xA where the goods/ring id's is 0x2 — so the id is the handle with its top nibble rewritten to 2 (verified 4/4 on the Joy ring set: 0xa0004e20 → 0x20004e20 = Life Ring, etc.). | |
| int | DS3_RING_ID_TYPE = 0x20000000 |
| dict | DS3_DB_FILES |
| DS3 id-scan tables: filename stem to category. | |
| int | DS3_ROSTER_PLAYTIME_OFF = 38 |
Play time (seconds, uint32) inside slot i's DS3 roster descriptor. | |
| int | DS3_GAITEM_START = 0x6C |
| int | DS3_GAITEM_SLOTS = 6144 |
| int | DS3_GAITEM_BIG = 60 |
| tuple | DS3_GAITEM_TYPES_BIG = (0x80000000, 0x90000000) |
| float | DS3_FLAG_MAX_DENSITY = 0.01 |
| int | DS3_FLAG_SAMPLE = 0x8000 |
| int | DS3_NG_MAX = 99 |
| DS3 New Game+ cycle (journey count), a uint16 just before the event-flag region, or None. | |
Dark Souls III.
| sl2.ds3.load_ds3_ring_effects | ( | base_dir | ) |
Definition at line 25 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_ring_effects().
| sl2.ds3.ds3_ring_level | ( | name | ) |
Split an equipped ring name into its table key and reinforcement level.
The db spells three of the four Ring of Favor ids "Ring of Favor+N" and the fourth "Ring of Favor +3", so the suffix is matched with the space optional.
(base name, level). Definition at line 41 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_ring_effects().
| sl2.ds3.ds3_attach_ring_effects | ( | ch, | |
| base_dir | |||
| ) |
Attach each equipped ring's documented effect, and the subset of those effects that is a structured modifier to a derived stat.
ch["ring_effects"] is [[name, effect text]] for the display; the text is the base line plus the worn level's own line, so a +2 ring shows what +2 does rather than only what the base ring does. ch["ring_mods"] is [[name, mods]] for the rings the derived stats fold in. Both are omitted entirely when nothing resolves, so a game without the table renders exactly as before.
| ch | The character dict. |
| base_dir | Folder holding db_ds3. |
Definition at line 55 of file ds3.py.
References sl2.ds3.ds3_ring_level(), and sl2.ds3.load_ds3_ring_effects().
| sl2.ds3.ds3_ring_bonuses | ( | ring_mods | ) |
Sum the worn rings' structured modifiers.
| ring_mods | ch["ring_mods"], or None. |
(totals, contributors) keyed by stat; contributors is {stat: [(ring name, value)]} so the render can name who supplied what. Definition at line 85 of file ds3.py.
Referenced by sl2.ds3.ds3_derived_stats().
| sl2.ds3.ds3_derived_stats | ( | stats, | |
ring_mods = None |
|||
| ) |
DS3 base derived stats that are closed-form functions of attributes only, plus the worn rings' documented bonuses to those same three values.
The attribute-only halves are the three the character screen shows that don't need gear: attunement slots (breakpoint count), base Equip Load (40 + Vitality) and base Item Discovery (100 + Luck, hard cap 199). HP/FP/stamina are read from the save, not recomputed; poise is gear-only in DS3, and defences, resistances and attack power are gear- and level-scaled, so none of those are derived here. Formulas from the fextralife Equipment Load / Attunement / Item Discovery pages.
Rings are folded in because these three values are NOT stored in the save, so unlike Max HP and stamina nothing else would ever show the bonus. Only the six rings with a structured mod count, and only into the value they name — a ring that boosts sorcery damage has nothing to add to a number printed here. Equip-load percentages are summed rather than compounded (DS3's equip-load rates are additive); the base and each contributor are kept so the render prints the sum's parts and the reader can check it. equip_load_base / item_discovery_base / slots_base keep the ringless figures.
| stats | The attribute dict. |
| ring_mods | ch["ring_mods"], or None. |
Definition at line 115 of file ds3.py.
References sl2.ds3.ds3_ring_bonuses().
| sl2.ds3.load_ds3_bonfires | ( | base_dir | ) |
Definition at line 148 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_lord_cinders | ( | base_dir | ) |
Definition at line 209 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_questlines | ( | base_dir | ) |
Definition at line 230 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_pickups | ( | base_dir | ) |
Definition at line 253 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_enemies | ( | base_dir | ) |
Definition at line 276 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_npcs | ( | base_dir | ) |
Definition at line 297 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.load_ds3_endings | ( | base_dir | ) |
Definition at line 345 of file ds3.py.
Referenced by sl2.ds3.ds3_attach_flags().
| sl2.ds3.ds3_goods_cat | ( | iid | ) |
Refine a DS3 goods id to its finer category (see DS3_GOODS_RANGES).
An id outside every block keeps goods, so an unmapped one is still printed.
Definition at line 483 of file ds3.py.
Referenced by sl2.ds3.ds3_item_cat().
| sl2.ds3.ds3_item_cat | ( | iid, | |
| cat | |||
| ) |
Refine a scanned DS3 id's category.
The one place that knows the id blocks differ from the file an id happens to be listed in.
Definition at line 504 of file ds3.py.
References sl2.ds3.ds3_goods_cat().
| sl2.ds3.scan_inventory | ( | buf, | |
| iddb | |||
| ) |
Find inventory by scanning for known item ids in fixed-size records.
Collects every offset whose uint32 is a known id, groups those on the same 16-byte record grid into runs (bridging holes left by untabled items, up to DS3_MAX_RUN_GAP), and keeps runs of at least SCAN_MIN_RUN. Each surviving record contributes its id and quantity. Duplicate ids are summed. A quantity outside a sane range drops the record — a cheap guard against a false run of look-alike bytes.
| buf | The decrypted slot data. |
| iddb | The flat id lookup from load_scan_db. |
(buckets, unknown_count), buckets mapping category to (name, qty). Definition at line 523 of file ds3.py.
References sl2.ds3.ds3_resolve_estus(), and sl2.ds3.ds3_resolve_weapon().
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_find_stats | ( | buf | ) |
Locate the DS3 stat block by content, or None if none validates.
DS3's stat offsets move between patches, so the block is not read from a fixed offset — it is found. For each 4-aligned position, treat the next nine uint32 as the attributes and accept only where each is 1..99 and their sum minus DS3_LEVEL_BASE equals the stored soul level. That identity is DS3's own level formula, so a coincidental match on unrelated bytes is not credible.
| buf | The decrypted slot data. |
Definition at line 578 of file ds3.py.
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_embered | ( | buf, | |
| v | |||
| ) |
DS3 embered state for a slot, or None.
| v | The stat anchor from ds3_find_stats (None → feature off). Reads the DS3_EMBER_D byte and only trusts a clean boolean: 1 → True (embered), 0 → False (hollow), anything else → None (never guess). |
Definition at line 601 of file ds3.py.
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_covenant | ( | buf, | |
| v | |||
| ) |
DS3 covenant name for a slot, or None.
| v | The stat anchor from ds3_find_stats (None → feature off). The field is an equip handle, so the covenant id is its low 28 bits and only ids in DS3_COVENANT are named — 0 means no covenant, and a cheated slot's junk handle resolves to nothing and is omitted rather than guessed. |
Definition at line 613 of file ds3.py.
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_resolve_weapon | ( | iddb, | |
| iid | |||
| ) |
Resolve an equipped DS3 weapon id to a name, unwrapping the baked "+N".
The GaItem array stores a weapon at its EXACT id, and reinforcement is folded into that id as base+infusion*100+level (the same scheme DS1 uses; a Deep Battle Axe +0/+1 read out as 7010900/7010901). Unlike DS1, the DS3 db keys every infusion by name (7010900 → "Deep Battle Axe"), so only the LEVEL (the units) is stripped — the base-infusion id already carries the infusion name. A direct hit wins; otherwise the level is peeled off and a " +N" suffix appended. The infusion itself is thus named for free; the level shows when > 0.
| iddb | The flat DS3 id lookup. |
| iid | The exact equipped-weapon id. |
Definition at line 686 of file ds3.py.
Referenced by sl2.ds3.ds3_equipped_weapons(), and sl2.ds3.scan_inventory().
| sl2.ds3.ds3_resolve_estus | ( | iid | ) |
Resolve a DS3 goods id to an Estus flask name with its +N, or None.
Only consulted after the id-scan table misses, so it can never shadow a real listed good.
| iid | The exact goods id. |
Definition at line 719 of file ds3.py.
Referenced by sl2.ds3.scan_inventory().
| sl2.ds3.ds3_gaitem_map | ( | buf | ) |
Map every GaItem handle to its item id by walking the GaItem array.
Equip slots reference items by handle; the array (same walk the event-flag base uses) is the handle→id table. Big records (weapon/armour types) are 60 bytes, everything else 8. Stops at the first unreadable slot.
| buf | The decrypted slot. |
Definition at line 747 of file ds3.py.
Referenced by sl2.ds3.ds3_equipped_ammo(), sl2.ds3.ds3_equipped_armor(), and sl2.ds3.ds3_equipped_weapons().
| sl2.ds3.ds3_equipped_weapons | ( | buf, | |
| iddb, | |||
| v | |||
| ) |
Equipped weapons (up to three per hand) from EquipGameData.
Reads the six handles at DS3_WEAPON_SLOTS, resolves each through the GaItem map, and keeps a slot only when it lands on a real weapons item that is not the bare DS3_FISTS (an empty hand reads Fists, not a null handle). Right/left labelling is verified by a weapon-swap differential; the resolved id carries both the infusion (a Deep weapon names itself) and the reinforcement, which ds3_resolve_weapon peels off as a " +N" suffix (verified by a Deep Battle Axe +0→+1 differential: id 7010900 → 7010901).
| buf | The decrypted slot. |
| iddb | The flat DS3 id lookup. |
| v | The stat anchor (None → feature off). |
Definition at line 774 of file ds3.py.
References sl2.ds3.ds3_gaitem_map(), and sl2.ds3.ds3_resolve_weapon().
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_equipped_armor | ( | buf, | |
| iddb, | |||
| v | |||
| ) |
Equipped armour (the four protection slots) from EquipGameData.
Reads the four handles at DS3_ARMOR_SLOTS, resolves each through the GaItem map, and keeps it only when it lands on a real armour item — a self-consistency gate: an equipped piece is one you own, and the category proves the slot read a protector and not a stray weapon/ring handle. Verified across the Joy ladder, including a Northern→Fallen Knight set change and a lone gauntlet swap, so the slots track real gear. Weapons, rings and the covenant slot are NOT read: their save layout does not match the runtime editor tables (the +0x50 slot holds a left-hand weapon, not the covenant), and pinning them needs a swap differential — omitted, not guessed.
| buf | The decrypted slot. |
| iddb | The flat DS3 id lookup. |
| v | The stat anchor (None → feature off). |
Definition at line 806 of file ds3.py.
References sl2.ds3.ds3_gaitem_map().
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_equipped_rings | ( | buf, | |
| iddb, | |||
| v | |||
| ) |
Equipped rings (up to four) from EquipGameData.
Each ring slot holds an accessory handle whose id is the handle with its type nibble rewritten from 0xA to 0x2 (DS3_RING_ID_TYPE) — rings are not in the GaItem array, so this direct transform stands in for a lookup. A slot is kept only when the derived id is a real rings item (self-consistency gate). The transform also carries a ring's reinforcement, so a +N ring names itself (the all-items mule reads "Ring of Steel Protection +3"). Verified across the Joy ladder, tracking her 2→4 ring growth.
| buf | The decrypted slot. |
| iddb | The flat DS3 id lookup. |
| v | The stat anchor (None → feature off). |
Definition at line 834 of file ds3.py.
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_equipped_ammo | ( | buf, | |
| iddb, | |||
| v | |||
| ) |
Equipped ammunition (the arrow/bolt quiver slots) from EquipGameData.
The four slots at DS3_AMMO_SLOTS hold GaItem handles resolved through the same handle→id map as armour; a slot is kept only when it lands on a bolts item (the db's shared arrow/bolt category), which gates out an empty or non-ammo slot. Verified on the Joy ladder (Standard Arrow/Bolt) and the all-items mule (Millwood Greatarrow, Exploding Bolt, …).
| buf | The decrypted slot. |
| iddb | The flat DS3 id lookup. |
| v | The stat anchor (None → feature off). |
Definition at line 861 of file ds3.py.
References sl2.ds3.ds3_gaitem_map().
Referenced by sl2.ds3.ds3_parse().
| sl2.ds3.ds3_parse | ( | buf, | |
| iddb, | |||
| name | |||
| ) |
Parse one DS3 slot into the unified dict (full tier where stats validate).
Inventory comes from the id-scan; the name is supplied by the caller from the load-screen roster. Stats are located by content (ds3_find_stats) and, when the level identity confirms them, promote the slot to full tier. When it does not (an unrecognised patch), stats are dropped and the slot stays inventory tier — a missing number beats a wrong one. Origin class and NG+ are not calibrated and are omitted. Returns None when the slot has no inventory.
| buf | The decrypted slot data. |
| iddb | The flat DS3 id lookup. |
| name | The character name from the roster, or None. |
Definition at line 888 of file ds3.py.
References sl2.ds3.ds3_covenant(), sl2.ds3.ds3_embered(), sl2.ds3.ds3_equipped_ammo(), sl2.ds3.ds3_equipped_armor(), sl2.ds3.ds3_equipped_rings(), sl2.ds3.ds3_equipped_weapons(), sl2.ds3.ds3_find_stats(), and sl2.ds3.scan_inventory().
| sl2.ds3.ds3_event_flag_base | ( | buf | ) |
Locate the DS3 event-flag region base in a decrypted slot, or None.
Walks the same block chain the alfizari editor uses. Every read is bounds-checked (u32 returns None past the buffer), so a short or edited save turns the feature off rather than reading garbage. The located base is then sanity-checked on bit density (see DS3_FLAG_MAX_DENSITY) — a mislocated base reads dense and is rejected, turning every flag feature off for that slot rather than inventing progress.
| sl2.ds3.ds3_attach_flags | ( | ch, | |
| buf, | |||
| base, | |||
| base_dir | |||
| ) |
Read DS3 bonfires + boss-defeat flags off the event-flag region and attach them to ch: bonfire_areas as [(area, count, [names])] — every lit bonfire named from load_ds3_bonfires — and merge flag boss evidence into ch["bosses"] (deduping with any soul/gate evidence already there, from both the per-map and the group-6 victory tables), cinders as the Lords of Cinder whose ashes are on the throne, and endings as the endings this character has reached (cumulative, so an NG+ save can hold several).
No-op if base is None (region not located).
| ch | A parsed character. |
| buf | The decrypted slot. |
| base | The event-flag base from ds3_event_flag_base. |
| base_dir | Repo root holding the db_* folders. |
Definition at line 1040 of file ds3.py.
References sl2.ds3.load_ds3_bonfires(), sl2.ds3.load_ds3_endings(), sl2.ds3.load_ds3_enemies(), sl2.ds3.load_ds3_lord_cinders(), sl2.ds3.load_ds3_npcs(), sl2.ds3.load_ds3_pickups(), and sl2.ds3.load_ds3_questlines().
| tuple sl2.ds3.DS3_SLOT_BREAKS = (10, 14, 18, 24, 30, 40, 50, 60, 80, 99) |
|
protected |
Load db_ds3/ring_effects.json: ring name → {"effect": [lines], "mods": {level: {stat: value}}}.
Effect text is one line for the base ring plus one per reinforcement level, verbatim from the fextralife Rings table; mods exists only for the six rings that move a value this tool derives. Generated by scratch/gen_ds3_ring_effects.py. Cached. Returns {} if absent.
|
protected |
Load the DS3 bonfire table (db_ds3/bonfires.json): area → list of [distance, bit, name], one entry per bonfire.
Distance is from the event-flag base, bit is within that byte. Generated from the SoulSplitter/TGA flag list via the confirmed flag-id→bit formula (each bonfire flag f: group f//1000, n=f%1000, byte = (n>>5)*4 + 3-((n&31)>>3), bit = 7-(n&7); save distance = memory byte + 111, one constant delta verified across all 16 groups against real saves). Cached. Returns {} if absent.
|
protected |
Load the DS3 boss-defeat flag table (db_ds3/boss_flags.json): boss name → [distance, bit] of its single "boss dead" event flag (the 13xxxx8xx per-map flag).
Generated from the SoulSplitter boss list via the same flag-id→bit formula as bonfires; every offset independently reproduced the old hand-checked table, so a set bit is a certain kill. Cached. Returns {} if absent.
|
protected |
Load the DS3 boss-victory flag table (db_ds3/boss_victory.json): boss name → [distance, bit] of its 63xx "boss victory" event flag, a SECOND independent kill signal that survives the soul being consumed.
These live in common group 6, whose save base (879) was derived from the Rosaria join differential and is confirmed here by fifteen more flags: walking the whole ladder, every one first turns on in exactly the snapshot the boss died in. Covers Stray Demon, which the per-map table does not. Cached. Returns {} if absent.
|
protected |
Load the DS3 Lords-of-Cinder table (db_ds3/lord_cinders.json): lord name → [distance, bit] of the flag set when that lord's Cinders are placed on the Firelink throne.
All four are listed now, each pinned by its own offering window (14000125 Abyss Watchers first — the only flag gained anywhere in the m40 group across a 46-second window — then Yhorm, Aldrich, and Twin Princes; they turned out to be the four ODD ids in one byte, which is what settled the last seat). Cached. Returns {}.
|
protected |
Load the DS3 NPC-questline table (db_ds3/questlines.json): NPC/source → list of [distance, bit, reward] — one per reward that NPC hands out, each a "you received this" event flag in the common group 50006.
The group-50006 save base (86639, region-relative) was derived empirically from a Hawkwood Heavy-Gem differential (the map-flag k*0x500 bases don't cover the common groups) and verified against a real save. A set flag means that reward was obtained — a questline-progress floor. Cached. Returns {} if absent.
|
protected |
Load the DS3 world item-pickup table (db_ds3/item_pickups.json): area → list of [distance, bit, item] — one per one-off item lying in that area, each a "you picked this up" event flag in the world groups 533xx–540xx.
Only SIX groups are in the file, the ones whose save base could be derived by windowed ladder timing (an item's flag must read 0 in every snapshot before the character first held it and 1 in every snapshot after, plus no flag in the group may ever clear); all six landed on the published k*0x500 + 111 grid, which is independent corroboration. Groups whose base is still unknown are ABSENT rather than guessed — a wrong base invents pickups. Cached. Returns {} if absent.
|
protected |
Load the one-time-enemy table (db_ds3/enemies.json): area → list of [distance, bit, enemy type] — one per enemy that does not respawn and whose death DS3 therefore has to remember.
Mimics, Crystal Lizards, Black Knights, the Boreal Outrider Knights. Extracted from the committed .emevd, where each is a call to a common_func template taking a death flag and an entity id, so these are DEATH flags rather than the drop flags the same enemies' loot already sets. Trusted because the count climbs 3 → 98 with no regressions across a 79-save ladder and the per-area breakdown tracks where the character actually went; see tools/gen_ds3_enemies.py. Unlike the per-map boss flags these survive a new journey. Cached. Returns {} if absent.
|
protected |
Load the DS3 NPC-state table (db_ds3/npcs.json): family → [[distance, bit, label]] — NPCs killed, NPCs turned hostile, questline states.
Generated by tools/gen_ds3_npcs.py, on the common-group base 111 + 128*g derived there and confirmed by a dated kill on the ladder. Two caveats live in that generator and are repeated in the section note: an English description can sit on the wrong flag (one demonstrably does), and a row with no description prints an entity id because inventing a name is worse. Cached. Returns {} if absent.
|
protected |
Load the DS3 covenant table (db_ds3/covenants.json, {covenant: [[region distance, bit, what it proves]]}).
Built from the same item-pickup flag list as the questlines, on the group-6 base (879) derived from a real Rosaria's-Fingers join differential and confirmed by the whole ladder reading chronologically (Way of Blue and Warrior of Sunlight appearing together at the High Wall → Undead Settlement step, Blue Sentinels at Road of Sacrifices, Rosaria only in the final save, and no DLC or rank flags anywhere). A set flag means the covenant was found, or that rank reward collected — a floor, like the questlines. Cached. Returns {} if absent.
|
protected |
Load the DS3 endings table (db_ds3/endings.json): ending name → [distance, bit].
All four endings live in ONE byte, one bit each — the same shape as the lord-cinder byte. Three bits were pinned by a three-way differential: the same pre-ending save was finished three different ways, and each ending flipped exactly one bit of it (bit 7 Link the First Flame, bit 5 The End of Fire, bit 4 the Fire-Keeper-slain variant), with nine further flips common to all three and so generic. The remaining bit 6 is The Usurpation of Fire BY ELIMINATION — the game has four endings, three seats are pinned by a labelled save, and the family is closed; it is the one field here not observed set, because reaching it needs a save from a playthrough that took it. Flags are cumulative, so a character can hold more than one. Cached. Returns {} if absent.
| sl2.ds3.DS3_RECORD |
| sl2.ds3.DS3_QTY_OFF |
| sl2.ds3.DS3_STAT_D |
DS3 stat block as signed distances from the Vigor field (the anchor).
Storage order is NOT the level-up display order: eight contiguous uint32 (Vigor, Attunement, Endurance, Strength, Dexterity, Intelligence, Faith, Luck), then Vitality alone after a two-field gap. Listed here in display order pointing at the true distances. Calibrated against a real lopsided build (Joy, STR 18 / VIT 14 / LCK 11 read out as VIT 18 / STR 9 / LCK 14 under the old naive mapping, which the order-independent level-sum identity could not catch).
| sl2.ds3.DS3_HP_D |
DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme.
HP/FP/stamina each store a current+max triple; these offsets point at the MAX copy (a lopsided real save read HP 728 max / 681 current at -40 / -36 — we take the max). FP at -28 verified 72 at ATN 6 and 450 at a high-attunement char.
| sl2.ds3.DS3_FP_D |
DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme.
HP/FP/stamina each store a current+max triple; these offsets point at the MAX copy (a lopsided real save read HP 728 max / 681 current at -40 / -36 — we take the max). FP at -28 verified 72 at ATN 6 and 450 at a high-attunement char.
| sl2.ds3.DS3_STAM_D |
DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme.
HP/FP/stamina each store a current+max triple; these offsets point at the MAX copy (a lopsided real save read HP 728 max / 681 current at -40 / -36 — we take the max). FP at -28 verified 72 at ATN 6 and 450 at a high-attunement char.
| sl2.ds3.DS3_LEVEL_D |
DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme.
HP/FP/stamina each store a current+max triple; these offsets point at the MAX copy (a lopsided real save read HP 728 max / 681 current at -40 / -36 — we take the max). FP at -28 verified 72 at ATN 6 and 450 at a high-attunement char.
| sl2.ds3.DS3_SOULS_D |
DS3 max HP, FP, stamina, soul level and souls, same anchor-relative scheme.
HP/FP/stamina each store a current+max triple; these offsets point at the MAX copy (a lopsided real save read HP 728 max / 681 current at -40 / -36 — we take the max). FP at -28 verified 72 at ATN 6 and 450 at a high-attunement char.
| int sl2.ds3.DS3_EMBER_D = 188 |
DS3 embered flag: a lone uint8 at +188 in the stat-mirror struct behind the stat anchor — 1 = embered, 0 = hollow.
Embered restores the ~30% Max HP bonus (which is why the stored Max HP at DS3_HP_D reads base*1.3 while this byte is 1), so reading it labels whether the Max HP figure is the embered or the base value. Pinned by a real Joy differential (using an Ember flipped 0->1 and Max HP 817->1062 = *1.30) and cross-checked on the all-items mule: its two embered slots read 1 with HP = base*1.3 (vig 99, 1400->1819) and its hollow slot reads 0 with base HP (vig 16, 594) — both polarities, HP corroborating each. Guarded to {0,1}; any other value omits the field.
| int sl2.ds3.DS3_COVENANT_D = 3944 |
DS3 covenant: a uint32 EQUIP HANDLE at +3944 from the stat anchor, not a small enum — the game models the covenant as a worn accessory, so the field holds 0xA00027xx and the covenant's own item id is the low 28 bits (Rosaria's Fingers 0xA0002760 -> 10080).
Ids and names come from the TGA Cheat Engine table's "Current Covenant" dropdown. Pinned by a real Joy join differential: the two saves are 37 seconds apart with the same level, bonfires and souls, and joining Rosaria's Fingers is the only thing that happened — the handle appears EXACTLY ONCE in the after save and NOT AT ALL in the before one. Cross-checked across the whole ladder (all 27 earlier saves read 0 = no covenant) and against the all-items mule, whose two cheated slots hold values that are not covenant ids at all and are dropped by the table lookup rather than printed.
| dict sl2.ds3.DS3_COVENANT |
| int sl2.ds3.DS3_LEVEL_BASE = 89 |
| int sl2.ds3.SCAN_MIN_RUN = 3 |
| int sl2.ds3.DS3_MAX_RUN_GAP = 48 |
Largest gap (bytes) a run may bridge.
The inventory is a 16-byte record grid, but records holding an UNTABLED id (not in our db) leave holes, so two known records can sit 32/48 bytes apart with the hole(s) between them. Bridging a gap that is a multiple of DS3_RECORD up to this bound keeps such a run whole, so an item flanked by unknowns on both sides (an island the strict stride-16 rule dropped) is still read. Observed holes are single-record (gap 32); the extra record of slack tolerates a rare double hole. Kept small so the run can't leap to unrelated on-grid coincidences. (Found via a Soul of a Stray Demon that read out present in the buffer but vanished from the list, wedged between two untabled items.)
| int sl2.ds3.DS3_GOODS_ID_BASE = 0x40000000 |
DS3 goods ids carry a type prefix (0x40000000); the real id is what the game's own EquipParamGoods table uses, and it blocks out cleanly by kind.
Splitting goods on those blocks gives DS3 the same finer categories DS2 already renders (upgrade / consumables / online / keys / boss souls) instead of one 100-row dump. Ranges read off the table itself (db_ds3/goods.json, sorted by id), not guessed: 100..119 soapstones+orbs+Darksign, 240..519 the whole consumable block, 520..524 the multiplayer carvings, 700..799 boss souls, 1000..1030 titanite, 1100..1250 infusion gems, 2001..2014 door keys, 2101..2159 tomes/coals/ashes/banners.
| tuple sl2.ds3.DS3_GOODS_RANGES |
| dict sl2.ds3.DS3_GOODS_OVERRIDE |
| sl2.ds3.DS3_AMMO_LO |
Where ammunition lives in the weapon id space.
Arrows and bolts ARE weapons to the param — and to Paramdex, which is where the full table comes from — but the report has always printed them under Ammunition, and ds3_equipped_ammo gates on that category, so they are re-homed on the way in. Bows start at 1300000, so the block is unambiguous.
| sl2.ds3.DS3_AMMO_HI |
Where ammunition lives in the weapon id space.
Arrows and bolts ARE weapons to the param — and to Paramdex, which is where the full table comes from — but the report has always printed them under Ammunition, and ds3_equipped_ammo gates on that category, so they are re-homed on the way in. Bows start at 1300000, so the block is unambiguous.
| int sl2.ds3.DS3_EQUIP_D = 664 |
| sl2.ds3.DS3_ARMOR_SLOTS |
| tuple sl2.ds3.DS3_RING_SLOTS = (0x34, 0x38, 0x3C, 0x40) |
| tuple sl2.ds3.DS3_AMMO_SLOTS = (0x08, 0x0C, 0x10, 0x14) |
| sl2.ds3.DS3_WEAPON_SLOTS |
The six weapon sub-offsets, in the game's own two-hand-by-slot order.
The struct interleaves the hands (LH1, RH1, LH2, RH2, LH3, RH3) starting 0x10 BEFORE the armour base — so relative to DS3_EQUIP_D the right hand is -0x0C/-0x04/+0x04 and the left is -0x10/-0x08/+0x00. Each holds a GaItem handle, resolved through the same map as armour/ammo. Pinned by a real weapon-swap differential (a Deep Battle Axe moved right→left read out at RH1 then LH1, everything else unchanged), retiring the old "hand slots unverifiable" blocker. The id carries the infusion (Deep = base+900), so an infused weapon names itself; the +N reinforcement lives in the 52-byte weapon record and is still not read.
| int sl2.ds3.DS3_FISTS = 110000 |
| int sl2.ds3.DS3_REINF_MAX = 10 |
| int sl2.ds3.DS3_GOODS_TYPE = 0x40000000 |
The two Estus flasks' base goods ids, and the reinforcement ceiling.
Like weapons, a flask's level is baked into its id, but it takes TWO consecutive ids per level (150/151 = Estus +0, 152/153 = +1 … 170/171 = +10; 190/191 = Ashen +0 … 210/211 = +10), so the flask is resolved arithmetically rather than listed — a name-keyed db cannot hold one name under two ids. Verified across Joy's 38-save ladder, where the id steps 151 → 153 → 155 → 157 → 159 (and the Ashen id 190 → 192 → 194 → 196 → 198 in lockstep), each step inside a 12-15 second window — one visit to Andre. Both members of a pair really occur: the all-items mule holds 171/211 (both +10) and 151/191, which is what pins the pairing and the endpoints.
| tuple sl2.ds3.DS3_ESTUS = ((150, "Estus Flask"), (190, "Ashen Estus Flask")) |
| int sl2.ds3.DS3_RING_ID_MASK = 0x0FFFFFFF |
A ring's equip handle encodes its own id: the low 28 bits are shared and the type nibble is 0xA where the goods/ring id's is 0x2 — so the id is the handle with its top nibble rewritten to 2 (verified 4/4 on the Joy ring set: 0xa0004e20 → 0x20004e20 = Life Ring, etc.).
Rings are NOT in the GaItem array (that array is weapons/armour only), which is why they need this direct transform instead of a handle lookup.
| dict sl2.ds3.DS3_DB_FILES |
| int sl2.ds3.DS3_ROSTER_PLAYTIME_OFF = 38 |
Play time (seconds, uint32) inside slot i's DS3 roster descriptor.
The status block does not carry it — it lives in the header menu block, at the same per-slot descriptor as the name, +38 past the descriptor start. Pinned by a ~17-minute differential (a real Joy save, 7573 s -> 8603 s, matching an on-screen 2:23:24). Per-character (one descriptor per slot).
| tuple sl2.ds3.DS3_GAITEM_TYPES_BIG = (0x80000000, 0x90000000) |
| int sl2.ds3.DS3_NG_MAX = 99 |
DS3 New Game+ cycle (journey count), a uint16 just before the event-flag region, or None.
new_game_plus sits at base + 0x12 - 0xBCC (base is that region less 0x12 — see ds3_event_flag_base). Guarded to a sane range: a cheated mule read 0xFFFF here, so an out-of-range value is omitted rather than printed wrong. Verified: Joy = 0 (New Game), a real NG+1 char = 1.