1"""Dark Souls II, vanilla and Scholar of the First Sin."""
5from collections
import OrderedDict, defaultdict
7from .crypto
import decrypt_ds2
8from .itemdb
import merge_qty
9from .reader
import is_valid_name, read_utf16, u8, u16, u32
18DS2_GAMES = (
"ds2sotfs",
"ds2vanilla")
23DS2_FAMILY = {
"dsr":
"ds1",
"ptde":
"ds1",
"ds2vanilla":
"ds2sotfs"}
27DS2_NAME_OFF, DS2_SOULS_OFF, DS2_SOULMEM_OFF, DS2_HP_OFF, DS2_NG_OFF = (
39DS2_TITLE_NAME_OFF, DS2_TITLE_STRIDE = 1286, 496
47DS2_TITLE_PLAYTIME_OFF = 66
54DS2_CLASS_OFF, DS2_COVENANT_OFF = 1024, 189
73 1:
"Heirs of the Sun",
75 3:
"Brotherhood of Blood",
80 8:
"Company of Champions",
81 9:
"Pilgrims of Dark",
93DS2_COV_DISC_D, DS2_COV_RANK_D, DS2_COV_MAX_RANK = 2, 12, 3
102DS2_GENDER_OFF, DS2_HOLLOW_OFF = 378, 379
116DS2_GENDER = {0:
"Male", 1:
"Female"}
127DS2_WORLD_ENTRY_DELTA, DS2_BONFIRE_FLAG_DELTA, DS2_BONFIRE_MIN_RUN = 10, 0x200, 16
136DS2_STAT_OFF = OrderedDict(
144 (
"Adaptability", 48),
145 (
"Intelligence", 44),
160DS2_STAMINA_BASE, DS2_EQUIP_BASE, DS2_AGL_BASE = 80, 38.5, 80
165DS2_IFRAMES = OrderedDict(
183DS2_SLOT_BREAKS = (10, 13, 16, 20, 25, 30, 40, 50, 60, 75, 94)
191DS2_PHYS_ATK_BP = OrderedDict(
214DS2_INV_RANGE, DS2_KEY_RANGE = (0x1E2C, 0x10E1C), (0x10E30, 0x11DF0)
232DS2_UPGRADEABLE = {
"weapons",
"armors"}
238DS2_REINF_OFF, DS2_INFUSE_OFF = 12, 13
263 "Old Paledrake Soul",
269 name = read_utf16(buf, DS2_NAME_OFF, 16)
270 return name
if is_valid_name(name)
else None
277 buckets, unknown = defaultdict(list), 0
278 for start, end
in (DS2_INV_RANGE, DS2_KEY_RANGE):
280 while o + 16 <= min(end, len(buf)):
286 iid, qty = u32(buf, o), u16(buf, o + 8)
287 cur, mx = u8(buf, o + 10), u8(buf, o + 11)
288 reinf = u8(buf, o + DS2_REINF_OFF)
289 infuse = u8(buf, o + DS2_INFUSE_OFF)
293 info = item_db.get(iid)
298 if name ==
"Estus Flask" and mx:
299 name = f
"{name} ({cur}/{mx} charges)"
300 if cat
in DS2_UPGRADEABLE:
305 if cat ==
"weapons" and infuse
in DS2_INFUSION:
306 name = f
"{DS2_INFUSION[infuse]} {name}"
308 name = f
"{name} +{reinf}"
309 buckets[cat].append((name, qty
if cat
in DS2_STACKABLE
else 1))
310 return buckets, unknown
316 stat = max(0, min(stat, 99))
317 lo = min((stat // 10) * 10, 90)
318 hi = 99
if lo == 90
else lo + 10
319 vlo, vhi = DS2_PHYS_ATK_BP[lo], DS2_PHYS_ATK_BP[hi]
320 return vlo
if hi == lo
else int(vlo + (vhi - vlo) * (stat - lo) / (hi - lo))
326 stat = max(0, min(stat, 99))
327 d = 6 * min(stat, 10)
329 d += 8 * (min(stat, 20) - 10)
331 d += 1 * (min(stat, 60) - 20)
333 d += (min(stat, 99) - 60) // 2
344def ds2_derived_stats(stats):
345 end = stats.get(
"Endurance", 0)
or 0
346 vit = stats.get(
"Vitality", 0)
or 0
347 adp = stats.get(
"Adaptability", 0)
or 0
348 atn = stats.get(
"Attunement", 0)
or 0
349 stg = stats.get(
"Strength", 0)
or 0
350 dex = stats.get(
"Dexterity", 0)
or 0
351 intel = stats.get(
"Intelligence", 0)
or 0
352 fth = stats.get(
"Faith", 0)
or 0
353 stamina = DS2_STAMINA_BASE + 2 * min(end, 20) + max(0, min(end, 99) - 20)
356 load = DS2_EQUIP_BASE + 1.5 * min(vit, 29)
358 load += 1.0 * (min(vit, 49) - 29)
360 load += 0.5 * (min(vit, 70) - 49)
362 load += 0.5 * ((min(vit, 99) - 70) // 2)
363 agl = DS2_AGL_BASE + int(0.75 * adp + 0.25 * atn + 1e-9)
365 for k, v
in DS2_IFRAMES.items():
368 slots = sum(1
for b
in DS2_SLOT_BREAKS
if atn >= b)
372 poise = 0.3 * min(n, 30)
374 poise += 0.2 * (min(n, 50) - 30)
376 poise += 0.1 * (min(n, 98) - 50)
406 for cid, name
in sorted(DS2_COVENANT.items()):
407 disc = u8(buf, DS2_COVENANT_OFF + DS2_COV_DISC_D + cid - 1)
408 rank = u8(buf, DS2_COVENANT_OFF + DS2_COV_RANK_D + cid - 1)
409 if disc
is None or rank
is None or disc > 1
or rank > DS2_COV_MAX_RANK:
411 if rank
and not disc:
414 out[name] = [f
"rank {rank} of {DS2_COV_MAX_RANK}" if rank
else "discovered"]
421def ds2_parse(buf, item_db, game="ds2sotfs"):
424 stats = OrderedDict((k, u16(buf, o)
or 0)
for k, o
in DS2_STAT_OFF.items())
426 inv = {c: merge_qty(v)
for c, v
in buckets.items()}
431 "klass": DS2_CLASS.get(u8(buf, DS2_CLASS_OFF)),
432 "covenant": DS2_COVENANT.get(u8(buf, DS2_COVENANT_OFF)),
434 "gender": DS2_GENDER.get(u8(buf, DS2_GENDER_OFF)),
435 "level": stats.pop(
"Level"),
437 "souls": u32(buf, DS2_SOULS_OFF),
438 "soul_memory": u32(buf, DS2_SOULMEM_OFF),
441 "hp": u32(buf, DS2_HP_OFF),
442 "ng_plus": max(0, (u16(buf, DS2_NG_OFF)
or 1) - 1),
443 "hollow_lvl": u8(buf, DS2_HOLLOW_OFF),
444 "deaths": u32(buf, DS2_DEATHS_OFF),
448 "key_items": inv.pop(
"keys", []),
450 "unknown_count": unknown,
456_DS2_BONFIRE_CACHE = {}
460 if base_dir
not in _DS2_BONFIRE_CACHE:
461 path = os.path.join(base_dir,
"db_ds2",
"bonfires.json")
463 with open(path, encoding=
"utf-8")
as f:
465 _DS2_BONFIRE_CACHE[base_dir] = {int(k, 16): v
for k, v
in raw.items()}
466 except (OSError, ValueError):
467 _DS2_BONFIRE_CACHE[base_dir] = {}
468 return _DS2_BONFIRE_CACHE[base_dir]
481 if base_dir
not in _DS2_AREA_CACHE:
482 path = os.path.join(base_dir,
"db_ds2",
"bonfire_areas.json")
484 with open(path, encoding=
"utf-8")
as f:
486 _DS2_AREA_CACHE[base_dir] = {int(k, 16): v
for k, v
in raw.items()}
487 except (OSError, ValueError):
488 _DS2_AREA_CACHE[base_dir] = {}
489 return _DS2_AREA_CACHE[base_dir]
497def load_ds2_bosses(base_dir):
498 if base_dir
not in _DS2_BOSS_CACHE:
499 path = os.path.join(base_dir,
"db_ds2",
"boss_flags.json")
501 with open(path, encoding=
"utf-8")
as f:
503 _DS2_BOSS_CACHE[base_dir] = {int(k, 16): v
for k, v
in raw.items()}
504 except (OSError, ValueError):
505 _DS2_BOSS_CACHE[base_dir] = {}
506 return _DS2_BOSS_CACHE[base_dir]
510_DS2_BOSS_SOUL_CACHE = {}
513def load_ds2_boss_souls(base_dir):
514 if base_dir
not in _DS2_BOSS_SOUL_CACHE:
515 path = os.path.join(base_dir,
"db_ds2",
"boss_souls.json")
517 with open(path, encoding=
"utf-8")
as f:
518 _DS2_BOSS_SOUL_CACHE[base_dir] = json.load(f)
519 except (OSError, ValueError):
520 _DS2_BOSS_SOUL_CACHE[base_dir] = {}
521 return _DS2_BOSS_SOUL_CACHE[base_dir]
537 "Unseen Path to Heide": (
"Dragonrider",),
538 "Undead Crypt Entrance": (
"Looking Glass Knight",
"Demon of Song"),
540 "Looking Glass Knight",
542 "Velstadt, the Royal Aegis",
555 "King's Ring": (
"Velstadt, the Royal Aegis",),
556 "Pharros Mask": (
"Blue Smelter Demon",),
557 "Flower Skirt": (
"Graverobber, Varg, and Cerah",),
568 "Velstadt, the Royal Aegis",
570 "Looking Glass Knight",
573 "Velstadt, the Royal Aegis",
575 "Looking Glass Knight",
578 "Velstadt, the Royal Aegis",
580 "Looking Glass Knight",
582 "Velstadt, the Royal Aegis": (
"Demon of Song",
"Looking Glass Knight"),
583 "Demon of Song": (
"Looking Glass Knight",),
593 head, sep, tail = name.rpartition(
" +")
594 return head
if sep
and tail.isdigit()
else name
610 out = defaultdict(set)
611 for off, name
in load_ds2_bosses(base_dir).items():
612 if world
and u8(world, off):
613 out[name].add(
"flag")
614 soul_db = load_ds2_boss_souls(base_dir)
615 for name, _qty
in ch[
"inv"].get(
"bosssouls", []):
616 boss = soul_db.get(name)
618 out[boss].add(
"soul")
619 for bonfire
in ch.get(
"bonfires")
or []:
620 for boss
in DS2_BOSS_GATE.get(bonfire, ()):
621 out[boss].add(
"gate")
624 held = {
ds2_base_name(n)
for items
in ch[
"inv"].values()
for n, _
in items}
625 held.update(n
for n, _
in ch.get(
"key_items", []))
626 for item, bosses
in DS2_ITEM_GATE.items():
629 out[boss].add(
"gate")
632 if (ch.get(
"ng_plus")
or 0) > 0:
633 out[
"Nashandra"].add(
"clear")
636 for boss
in list(out):
637 for pre
in DS2_BOSS_PREREQ.get(boss, ()):
641 return {b: sorted(out[b])
for b
in sorted(out)}
653 if not world
or not bf_db:
655 best_start, best_run, run, run_start, o = -1, 0, 0, 0, 0
656 while o + 2 <= len(world):
657 if u16(world, o)
in bf_db:
658 run_start = o
if run == 0
else run_start
661 best_run, best_start = run, run_start
665 if best_run < DS2_BONFIRE_MIN_RUN:
669 while o + 2 <= len(world)
and len(ids) < DS2_BONFIRE_FLAG_DELTA // 2:
675 flag_base = best_start + DS2_BONFIRE_FLAG_DELTA
677 for idx, bid
in enumerate(ids):
678 if u8(world, flag_base + idx):
679 visited.append((bid, bf_db.get(bid, f
"(bonfire {bid:#06x})")))
691 if not visited
or not area_db:
693 seen = {bid
for bid, _n
in visited}
694 order = [area_db[bid]
for bid, _n
in visited
if bid
in area_db]
695 areas = OrderedDict((a, ([], []))
for a
in order)
698 for bid, area
in area_db.items():
699 got, miss = areas.setdefault(area, ([], []))
700 name = bf_db.get(bid)
703 (got
if bid
in seen
else miss).append(name)
705 (a, len(got), got, len(got) + len(miss), miss)
706 for a, (got, miss)
in areas.items()
708 return out
if any(c
for _a, c, _n, _t, _m
in out)
else None
715def ds2_augment(ch, data, entries, i, base_dir, dec=decrypt_ds2):
719 hdr = dec(data[entries[0].offset : entries[0].offset + entries[0].size])
721 base = DS2_TITLE_NAME_OFF + DS2_TITLE_STRIDE * (i - 1)
722 ch[
"play_time"] = u32(hdr, base + DS2_TITLE_PLAYTIME_OFF)
723 w = i + DS2_WORLD_ENTRY_DELTA
724 if w >= len(entries):
726 world = dec(data[entries[w].offset : entries[w].offset + entries[w].size])
730 ch[
"bonfires"] = [name
for _, name
in visited]
if visited
else visited
735 ch[
"bonfire_areas"] = areas
752def ds2_active_slots(data, entries, slots, dec=decrypt_ds2):
755 hdr = dec(data[entries[0].offset : entries[0].offset + entries[0].size])
760 off = DS2_TITLE_NAME_OFF + DS2_TITLE_STRIDE * (i - slots.start)
761 if is_valid_name(read_utf16(hdr, off, 16)):
763 return active
or None
ds2_phys_atk(stat)
Physical attack bonus (ATK: Str/Dex) at a stat value: linear-interpolate the decade breakpoints of DS...
ds2_elem_def(stat)
Shared DS2 elemental-defence curve: +6/pt to 10, +8/pt to 20, +1/pt to 60, +0.5/pt (one every other) ...
ds2_infer_bosses(world, ch, base_dir)
Bosses this character has defeated, as {boss: [evidence]}, or None.
ds2_name(buf)
Read a DS2 name, or None for an empty slot.
ds2_base_name(name)
An inventory name with its " +N" reinforcement suffix removed.
load_ds2_bonfires(base_dir)
ds2_inventory(buf, item_db)
Sort both DS2 inventory regions into categories.
ds2_visited_bonfires(world, bf_db)
Names of the bonfires this character has discovered, or None.
load_ds2_bonfire_areas(base_dir)
ds2_covenants(buf)
Every DS2 covenant the character has discovered, with its rank.
ds2_bonfire_areas(visited, area_db, bf_db)
Group discovered bonfires by area, in the (area, count, names) shape DS1 and DS3 already emit,...