6from collections
import OrderedDict, defaultdict
8from .progress
import find_boss_souls
9from .reader
import is_valid_name, u8, u16, u32
10from .roster
import ROSTER_PARAMS
14DS3_SLOT_BREAKS = (10, 14, 18, 24, 30, 40, 50, 60, 80, 99)
26 if base_dir
not in _DS3_RING_CACHE:
27 path = os.path.join(base_dir,
"db_ds3",
"ring_effects.json")
29 with open(path, encoding=
"utf-8")
as f:
30 _DS3_RING_CACHE[base_dir] = json.load(f)
32 _DS3_RING_CACHE[base_dir] = {}
33 return _DS3_RING_CACHE[base_dir]
42 m = re.match(
r"^(.*?)\s*\+(\d)$", name)
43 return (m.group(1), int(m.group(2)))
if m
else (name, 0)
55def ds3_attach_ring_effects(ch, base_dir):
59 effects, mods = [], []
60 for name
in ch.get(
"equipped_rings")
or ():
62 entry = table.get(base)
65 lines = entry[
"effect"]
67 for extra
in lines[1:]:
68 if extra.startswith(f
"+{lvl}:"):
70 effects.append([name, text])
71 m = (entry.get(
"mods")
or {}).get(str(lvl))
73 mods.append([name, m])
75 ch[
"ring_effects"] = effects
77 ch[
"ring_mods"] = mods
86 keys = (
"load_pct",
"discovery",
"slots",
"hp_pct",
"stam_pct")
87 total = dict.fromkeys(keys, 0)
88 who = {k: []
for k
in keys}
89 for name, mods
in ring_mods
or ():
90 for k, v
in mods.items():
93 who[k].append((name, v))
115def ds3_derived_stats(stats, ring_mods=None):
116 atn = stats.get(
"Attunement", 0)
or 0
117 vit = stats.get(
"Vitality", 0)
or 0
118 lck = stats.get(
"Luck", 0)
or 0
120 base_load = 40.0 + vit
123 load = int(base_load * (100.0 + total[
"load_pct"]) / 10.0) / 10.0
124 slots = sum(1
for b
in DS3_SLOT_BREAKS
if atn >= b)
126 discovery = min(199, 100 + lck)
128 "slots": slots + int(total[
"slots"]),
131 "equip_load_base": base_load,
132 "item_discovery": int(discovery + total[
"discovery"]),
133 "item_discovery_base": discovery,
145_DS3_BONFIRE_CACHE = {}
149 if base_dir
not in _DS3_BONFIRE_CACHE:
150 path = os.path.join(base_dir,
"db_ds3",
"bonfires.json")
152 with open(path, encoding=
"utf-8")
as f:
153 _DS3_BONFIRE_CACHE[base_dir] = json.load(f)
154 except (OSError, ValueError):
155 _DS3_BONFIRE_CACHE[base_dir] = {}
156 return _DS3_BONFIRE_CACHE[base_dir]
167def load_ds3_boss_flags(base_dir):
168 if base_dir
not in _DS3_BOSS_CACHE:
169 path = os.path.join(base_dir,
"db_ds3",
"boss_flags.json")
171 with open(path, encoding=
"utf-8")
as f:
172 _DS3_BOSS_CACHE[base_dir] = json.load(f)
173 except (OSError, ValueError):
174 _DS3_BOSS_CACHE[base_dir] = {}
175 return _DS3_BOSS_CACHE[base_dir]
185_DS3_VICTORY_CACHE = {}
188def load_ds3_boss_victory(base_dir):
189 if base_dir
not in _DS3_VICTORY_CACHE:
190 path = os.path.join(base_dir,
"db_ds3",
"boss_victory.json")
192 with open(path, encoding=
"utf-8")
as f:
193 _DS3_VICTORY_CACHE[base_dir] = json.load(f)
194 except (OSError, ValueError):
195 _DS3_VICTORY_CACHE[base_dir] = {}
196 return _DS3_VICTORY_CACHE[base_dir]
206_DS3_CINDER_CACHE = {}
210 if base_dir
not in _DS3_CINDER_CACHE:
211 path = os.path.join(base_dir,
"db_ds3",
"lord_cinders.json")
213 with open(path, encoding=
"utf-8")
as f:
214 _DS3_CINDER_CACHE[base_dir] = json.load(f)
215 except (OSError, ValueError):
216 _DS3_CINDER_CACHE[base_dir] = {}
217 return _DS3_CINDER_CACHE[base_dir]
231 if base_dir
not in _DS3_QUEST_CACHE:
232 path = os.path.join(base_dir,
"db_ds3",
"questlines.json")
234 with open(path, encoding=
"utf-8")
as f:
235 _DS3_QUEST_CACHE[base_dir] = json.load(f)
236 except (OSError, ValueError):
237 _DS3_QUEST_CACHE[base_dir] = {}
238 return _DS3_QUEST_CACHE[base_dir]
250_DS3_PICKUP_CACHE = {}
254 if base_dir
not in _DS3_PICKUP_CACHE:
255 path = os.path.join(base_dir,
"db_ds3",
"item_pickups.json")
257 with open(path, encoding=
"utf-8")
as f:
258 _DS3_PICKUP_CACHE[base_dir] = json.load(f)
259 except (OSError, ValueError):
260 _DS3_PICKUP_CACHE[base_dir] = {}
261 return _DS3_PICKUP_CACHE[base_dir]
277 if base_dir
not in _DS3_ENEMY_CACHE:
278 path = os.path.join(base_dir,
"db_ds3",
"enemies.json")
280 with open(path, encoding=
"utf-8")
as f:
281 _DS3_ENEMY_CACHE[base_dir] = json.load(f)
282 except (OSError, ValueError):
283 _DS3_ENEMY_CACHE[base_dir] = {}
284 return _DS3_ENEMY_CACHE[base_dir]
298 if base_dir
not in _DS3_NPC_CACHE:
299 path = os.path.join(base_dir,
"db_ds3",
"npcs.json")
301 with open(path, encoding=
"utf-8")
as f:
302 _DS3_NPC_CACHE[base_dir] = json.load(f)
303 except (OSError, ValueError):
304 _DS3_NPC_CACHE[base_dir] = {}
305 return _DS3_NPC_CACHE[base_dir]
320def load_ds3_covenants(base_dir):
321 if base_dir
not in _DS3_COV_CACHE:
322 path = os.path.join(base_dir,
"db_ds3",
"covenants.json")
324 with open(path, encoding=
"utf-8")
as f:
325 _DS3_COV_CACHE[base_dir] = json.load(f)
326 except (OSError, ValueError):
327 _DS3_COV_CACHE[base_dir] = {}
328 return _DS3_COV_CACHE[base_dir]
342_DS3_ENDING_CACHE = {}
346 if base_dir
not in _DS3_ENDING_CACHE:
347 path = os.path.join(base_dir,
"db_ds3",
"endings.json")
349 with open(path, encoding=
"utf-8")
as f:
350 _DS3_ENDING_CACHE[base_dir] = json.load(f)
351 except (OSError, ValueError):
352 _DS3_ENDING_CACHE[base_dir] = {}
353 return _DS3_ENDING_CACHE[base_dir]
357DS3_RECORD, DS3_QTY_OFF = 16, 4
367DS3_STAT_D = OrderedDict(
375 (
"Intelligence", 20),
386DS3_HP_D, DS3_FP_D, DS3_STAM_D, DS3_LEVEL_D, DS3_SOULS_D = -40, -28, -12, 44, 48
415 10000:
"Blade of the Darkmoon",
416 10020:
"Watchdogs of Farron",
417 10030:
"Aldrich Faithful",
418 10040:
"Warrior of Sunlight",
419 10050:
"Mound-makers",
420 10060:
"Way of Blue",
421 10070:
"Blue Sentinels",
422 10080:
"Rosaria's Fingers",
423 10090:
"Spears of the Church",
459DS3_GOODS_ID_BASE = 0x40000000
463 (100, 149,
"online"),
464 (150, 519,
"consumables"),
465 (520, 599,
"online"),
466 (600, 699,
"consumables"),
467 (700, 799,
"bosssouls"),
468 (1000, 1299,
"upgrade"),
469 (2000, 2199,
"keys"),
474DS3_GOODS_OVERRIDE = {
484 real = iid - DS3_GOODS_ID_BASE
485 if real
in DS3_GOODS_OVERRIDE:
486 return DS3_GOODS_OVERRIDE[real]
487 for lo, hi, cat
in DS3_GOODS_RANGES:
498DS3_AMMO_LO, DS3_AMMO_HI = 400000, 409999
504def ds3_item_cat(iid, cat):
507 if cat ==
"weapons" and DS3_AMMO_LO <= iid <= DS3_AMMO_HI:
526 for o
in range(0, len(buf) - 8)
527 if int.from_bytes(buf[o : o + 4],
"little")
in iddb
529 buckets, seen, unknown = defaultdict(dict), set(), 0
530 i, n = 0, len(positions)
534 d = positions[j + 1] - positions[j]
535 if d % DS3_RECORD == 0
and d <= DS3_MAX_RUN_GAP:
539 if j - i + 1 >= SCAN_MIN_RUN:
546 for o
in range(positions[i], positions[j] + 1, DS3_RECORD):
550 iid = int.from_bytes(buf[o : o + 4],
"little")
551 qty = u32(buf, o + DS3_QTY_OFF)
or 0
552 if not 1 <= qty <= 9999:
554 entry = iddb.get(iid)
558 if reinf
is None and estus
is None:
561 entry = (reinf,
"weapons")
if reinf
else (estus,
"consumables")
563 bucket = buckets[cat]
564 bucket[name] = bucket.get(name, 0) + qty
566 return {c: list(v.items())
for c, v
in buckets.items()}, unknown
579 dists = list(DS3_STAT_D.values())
580 v, end = 0, len(buf) - DS3_SOULS_D - 4
583 if first
is not None and 1 <= first <= 99:
584 vals = [u32(buf, v + d)
for d
in dists]
585 lvl = u32(buf, v + DS3_LEVEL_D)
587 all(x
is not None and 1 <= x <= 99
for x
in vals)
590 and sum(vals) - DS3_LEVEL_BASE == lvl
604 e = u8(buf, v + DS3_EMBER_D)
605 return True if e == 1
else False if e == 0
else None
616 h = u32(buf, v + DS3_COVENANT_D)
617 return DS3_COVENANT.get(h & 0x0FFFFFFF)
if h
else None
628DS3_ARMOR_SLOTS = OrderedDict(
629 [(
"Head", 0x20), (
"Chest", 0x24), (
"Hands", 0x28), (
"Legs", 0x2C)]
634DS3_RING_SLOTS = (0x34, 0x38, 0x3C, 0x40)
640DS3_AMMO_SLOTS = (0x08, 0x0C, 0x10, 0x14)
653DS3_WEAPON_SLOTS = OrderedDict(
655 (
"Right Hand", -0x0C),
656 (
"Right Hand 2", -0x04),
657 (
"Right Hand 3", 0x04),
658 (
"Left Hand", -0x10),
659 (
"Left Hand 2", -0x08),
660 (
"Left Hand 3", 0x00),
687 entry = iddb.get(iid)
688 if entry
and entry[1] ==
"weapons":
691 if not 1 <= level <= DS3_REINF_MAX:
693 base = iddb.get(iid - level)
694 return f
"{base[0]} +{level}" if base
and base[1] ==
"weapons" else None
706DS3_GOODS_TYPE = 0x40000000
709DS3_ESTUS = ((150,
"Estus Flask"), (190,
"Ashen Estus Flask"))
720 raw = iid - DS3_GOODS_TYPE
721 for base, name
in DS3_ESTUS:
722 level = (raw - base) // 2
723 if raw >= base
and 0 <= level <= DS3_ESTUS_MAX:
724 return name
if level == 0
else f
"{name} +{level}"
734DS3_RING_ID_MASK = 0x0FFFFFFF
737DS3_RING_ID_TYPE = 0x20000000
748 out, off = {}, DS3_GAITEM_START
749 for _
in range(DS3_GAITEM_SLOTS):
750 handle = u32(buf, off)
753 iid = u32(buf, off + 4)
756 big = handle
and (handle & 0xF0000000)
in DS3_GAITEM_TYPES_BIG
757 off += DS3_GAITEM_BIG
if big
else 8
778 base = v + DS3_EQUIP_D
780 for slot, d
in DS3_WEAPON_SLOTS.items():
781 handle = u32(buf, base + d)
782 iid = hmap.get(handle)
if handle
else None
783 if not iid
or iid == DS3_FISTS:
810 base = v + DS3_EQUIP_D
812 for slot, d
in DS3_ARMOR_SLOTS.items():
813 handle = u32(buf, base + d)
814 iid = hmap.get(handle)
if handle
else None
815 entry = iddb.get(iid)
if iid
else None
816 if entry
and entry[1] ==
"armors":
837 base = v + DS3_EQUIP_D
839 for d
in DS3_RING_SLOTS:
840 handle = u32(buf, base + d)
843 iid = (handle & DS3_RING_ID_MASK) | DS3_RING_ID_TYPE
844 entry = iddb.get(iid)
845 if entry
and entry[1] ==
"rings":
865 base = v + DS3_EQUIP_D
867 for d
in DS3_AMMO_SLOTS:
868 handle = u32(buf, base + d)
869 iid = hmap.get(handle)
if handle
else None
870 entry = iddb.get(iid)
if iid
else None
871 if entry
and entry[1] ==
"bolts":
888def ds3_parse(buf, iddb, name):
895 goods = inv.get(
"bosssouls", []) + inv.get(
"goods", [])
896 key_items = inv.pop(
"keys", [])
899 OrderedDict((k, u32(buf, v + d))
for k, d
in DS3_STAT_D.items())
904 "tier":
"full" if stats
else "inventory",
906 "name": name
if (name
and is_valid_name(name))
else "(unnamed slot)",
912 "level": u32(buf, v + DS3_LEVEL_D)
if v
is not None else None,
913 "souls": u32(buf, v + DS3_SOULS_D)
if v
is not None else None,
914 "stamina": u32(buf, v + DS3_STAM_D)
if v
is not None else None,
915 "hp": u32(buf, v + DS3_HP_D)
if v
is not None else None,
916 "fp": u32(buf, v + DS3_FP_D)
if v
is not None else None,
923 "boss_souls": find_boss_souls(goods),
924 "key_items": key_items,
932 "weapons":
"weapons",
946DS3_ROSTER_PLAYTIME_OFF = 38
949def ds3_playtime(menu_data, i):
950 p = ROSTER_PARAMS[
"ds3"]
951 return u32(menu_data, p[
"desc"] + p[
"stride"] * i + DS3_ROSTER_PLAYTIME_OFF)
968DS3_GAITEM_START = 0x6C
971DS3_GAITEM_SLOTS = 6144
977DS3_GAITEM_TYPES_BIG = (0x80000000, 0x90000000)
988DS3_FLAG_MAX_DENSITY = 0.01
991DS3_FLAG_SAMPLE = 0x8000
1002def ds3_event_flag_base(buf):
1003 off = DS3_GAITEM_START
1004 for _
in range(DS3_GAITEM_SLOTS):
1005 handle = u32(buf, off)
1008 big = handle
and (handle & 0xF0000000)
in DS3_GAITEM_TYPES_BIG
1009 off += DS3_GAITEM_BIG
if big
else 8
1010 above_counter = off + 0x13F + 0x1DD + 0x8808 + 0x11C
1011 above_size = u32(buf, above_counter)
1012 if above_size
is None:
1014 gesture_end = above_counter + 4 + above_size * 8 + 0x18C + 0x4 + 0x8800 + 0xC + 0xA4
1015 table2_size = u32(buf, gesture_end)
1016 if table2_size
is None:
1018 base = gesture_end + 4 + table2_size * 4 + 0x92 + 0xBCC - 0x12
1019 if not 0 <= base < len(buf):
1021 sample = buf[base : base + DS3_FLAG_SAMPLE]
1024 density = sum(bin(b).count(
"1")
for b
in sample) / (len(sample) * 8)
1025 return base
if density <= DS3_FLAG_MAX_DENSITY
else None
1040def ds3_attach_flags(ch, buf, base, base_dir):
1043 areas, any_lit = [],
False
1045 named, missing = [], []
1046 for dist, bit, name
in bonfires:
1047 val = u8(buf, base + dist)
1048 (named
if val
is not None and val & (1 << bit)
else missing).append(name)
1049 any_lit = any_lit
or bool(named)
1050 areas.append((area, len(named), named, len(bonfires), missing))
1054 ch[
"bonfire_areas"] = areas
1055 bosses = {b: set(s)
for b, s
in (ch.get(
"bosses")
or {}).items()}
1056 for table
in (load_ds3_boss_flags(base_dir), load_ds3_boss_victory(base_dir)):
1057 for name, (dist, bit)
in table.items():
1058 val = u8(buf, base + dist)
1059 if val
is not None and val & (1 << bit):
1060 bosses.setdefault(name, set()).add(
"flag")
1062 ch[
"bosses"] = {b: sorted(bosses[b])
for b
in bosses}
1066 if (u8(buf, base + dist)
or 0) & (1 << bit)
1069 ch[
"cinders"] = cinders
1070 quests = OrderedDict()
1073 rw
for dist, bit, rw
in rewards
if (u8(buf, base + dist)
or 0) & (1 << bit)
1078 ch[
"questlines"] = quests
1081 picks, any_found = [],
False
1083 got, missing = [], []
1084 for dist, bit, item, where
in items:
1085 val = u8(buf, base + dist)
1089 label = f
"{item} — {where}" if where
else item
1090 (got
if val
is not None and val & (1 << bit)
else missing).append(label)
1091 any_found = any_found
or bool(got)
1092 picks.append((area, len(got), len(items), missing))
1094 ch[
"pickups"] = picks
1097 foes, any_dead = [],
False
1099 dead, alive = [], []
1100 for dist, bit, name
in enemies:
1101 val = u8(buf, base + dist)
1102 (dead
if val
is not None and val & (1 << bit)
else alive).append(name)
1103 any_dead = any_dead
or bool(dead)
1104 foes.append((area, len(dead), dead, len(enemies), alive))
1106 ch[
"enemies"] = foes
1114 for dist, bit, label
in marks:
1115 val = u8(buf, base + dist)
1116 if val
is not None and val & (1 << bit):
1118 npcs.append((family, len(got), got, len(marks)))
1119 if any(c
for _f, c, _g, _t
in npcs):
1120 ch[
"npc_states"] = npcs
1122 covs = OrderedDict()
1123 for cov, marks
in load_ds3_covenants(base_dir).items():
1126 for dist, bit, what
in marks
1127 if (u8(buf, base + dist)
or 0) & (1 << bit)
1132 ch[
"covenants"] = covs
1137 if (u8(buf, base + dist)
or 0) & (1 << bit)
1140 ch[
"endings"] = endings
1151def ds3_journey(buf, base):
1154 ng = u16(buf, base + 0x12 - 0xBCC)
1155 return ng
if ng
is not None and 0 <= ng <= DS3_NG_MAX
else None
load_ds3_ring_effects(base_dir)
ds3_equipped_weapons(buf, iddb, v)
Equipped weapons (up to three per hand) from EquipGameData.
ds3_equipped_rings(buf, iddb, v)
Equipped rings (up to four) from EquipGameData.
ds3_covenant(buf, v)
DS3 covenant name for a slot, or None.
load_ds3_endings(base_dir)
load_ds3_enemies(base_dir)
ds3_resolve_estus(iid)
Resolve a DS3 goods id to an Estus flask name with its +N, or None.
ds3_goods_cat(iid)
Refine a DS3 goods id to its finer category (see DS3_GOODS_RANGES).
ds3_gaitem_map(buf)
Map every GaItem handle to its item id by walking the GaItem array.
ds3_equipped_armor(buf, iddb, v)
Equipped armour (the four protection slots) from EquipGameData.
ds3_resolve_weapon(iddb, iid)
Resolve an equipped DS3 weapon id to a name, unwrapping the baked "+N".
load_ds3_questlines(base_dir)
ds3_equipped_ammo(buf, iddb, v)
Equipped ammunition (the arrow/bolt quiver slots) from EquipGameData.
ds3_ring_level(name)
Split an equipped ring name into its table key and reinforcement level.
ds3_ring_bonuses(ring_mods)
Sum the worn rings' structured modifiers.
ds3_embered(buf, v)
DS3 embered state for a slot, or None.
ds3_find_stats(buf)
Locate the DS3 stat block by content, or None if none validates.
load_ds3_bonfires(base_dir)
scan_inventory(buf, iddb)
Find inventory by scanning for known item ids in fixed-size records.
load_ds3_pickups(base_dir)
load_ds3_lord_cinders(base_dir)