1"""Dark Souls 1 family: Remastered and Prepare to Die Edition."""
5from collections
import OrderedDict, defaultdict
7from .itemdb
import merge_qty
8from .progress
import attach_defeated_bosses, find_boss_souls, find_key_goods
9from .reader
import is_valid_name, read_utf16, u8, u16, u32
19def ds1_derived_stats(stats):
20 end = stats.get(
"Endurance", 0)
or 0
21 atn = stats.get(
"Attunement", 0)
or 0
23 "slots": sum(1
for b
in DS1_SLOT_BREAKS
if atn >= b),
24 "equip_load": float(DS1_EQUIP_BASE + end),
30_DS1_BONFIRE_CACHE = {}
34 if base_dir
not in _DS1_BONFIRE_CACHE:
35 path = os.path.join(base_dir,
"db_ds1",
"bonfires.json")
37 with open(path, encoding=
"utf-8")
as f:
38 _DS1_BONFIRE_CACHE[base_dir] = {
39 int(k): tuple(v)
for k, v
in json.load(f).items()
41 except (OSError, ValueError):
42 _DS1_BONFIRE_CACHE[base_dir] = {}
43 return _DS1_BONFIRE_CACHE[base_dir]
48_DS1_BOSSFLAG_CACHE = {}
51def load_ds1_boss_flags(base_dir):
52 if base_dir
not in _DS1_BOSSFLAG_CACHE:
53 path = os.path.join(base_dir,
"db_ds1",
"boss_flags.json")
55 with open(path, encoding=
"utf-8")
as f:
56 _DS1_BOSSFLAG_CACHE[base_dir] = json.load(f)
57 except (OSError, ValueError):
58 _DS1_BOSSFLAG_CACHE[base_dir] = {}
59 return _DS1_BOSSFLAG_CACHE[base_dir]
70DS1_FLAG_BASE = {
"dsr": 127721,
"ptde": 127273}
77DS1_FLAG_MAX_DENSITY, DS1_FLAG_SPAN = 0.05, 23156
88 base = DS1_FLAG_BASE.get(game)
89 table = load_ds1_boss_flags(base_dir)
90 if base
is None or not table
or base + DS1_FLAG_SPAN > len(buf):
92 region = buf[base : base + DS1_FLAG_SPAN]
93 if sum(bin(b).count(
"1")
for b
in region) > len(region) * 8 * DS1_FLAG_MAX_DENSITY:
95 bosses = {b: set(s)
for b, s
in (ch.get(
"bosses")
or {}).items()}
96 for name, (off, mask)
in table.items():
97 v = u32(buf, base + off)
98 if v
is not None and v & mask:
99 bosses.setdefault(name, set()).add(
"flag")
101 ch[
"bosses"] = {b: sorted(bosses[b])
for b
in bosses}
107 got, missing = [], []
108 for off, mask, name
in rows:
109 v = u32(buf, base + off)
110 (got
if v
is not None and v & mask
else missing).append(name)
111 world.append((cat, len(got), got, len(rows), missing))
112 if any(c
for _a, c, _n, _t, _m
in world):
113 ch[
"world_flags"] = world
121 got = sum(1
for off, mask, _n
in rows
if (u32(buf, base + off)
or 0) & mask)
122 events.append((cat, got, len(rows)))
123 if any(c
for _a, c, _t
in events):
124 ch[
"world_events"] = events
132_KNOWN_FLAG_CACHE = {}
136 if base_dir
not in _KNOWN_FLAG_CACHE:
137 path = os.path.join(base_dir,
"db_ds1",
"known_flags.json")
139 with open(path, encoding=
"utf-8")
as f:
140 _KNOWN_FLAG_CACHE[base_dir] = json.load(f)
141 except (OSError, ValueError):
142 _KNOWN_FLAG_CACHE[base_dir] = {}
143 return _KNOWN_FLAG_CACHE[base_dir]
153_WORLD_EVENT_CACHE = {}
157 if base_dir
not in _WORLD_EVENT_CACHE:
158 path = os.path.join(base_dir,
"db_ds1",
"world_events.json")
160 with open(path, encoding=
"utf-8")
as f:
161 _WORLD_EVENT_CACHE[base_dir] = json.load(f)
162 except (OSError, ValueError):
163 _WORLD_EVENT_CACHE[base_dir] = {}
164 return _WORLD_EVENT_CACHE[base_dir]
170DS1_BONFIRE_REC, DS1_BONFIRE_STATE_D = 20, 4
185DS1_BONFIRE_MIN_RUN = 5
200 while o + DS1_BONFIRE_REC <= len(buf):
201 if u32(buf, o)
in db:
202 run, p, seen = [], o, set()
203 while p + DS1_BONFIRE_REC <= len(buf):
205 state = u32(buf, p + DS1_BONFIRE_STATE_D)
206 if bid
not in db
or state
not in DS1_BONFIRE_STATE
or bid
in seen:
209 run.append((bid, state))
211 if len(run) > len(best):
216 if len(best) < DS1_BONFIRE_MIN_RUN:
218 found = {bid: state
for bid, state
in best}
219 areas = OrderedDict()
220 for bid, (name, area)
in db.items():
221 got, miss = areas.setdefault(area, ([], []))
223 got.append(f
"{name} ({DS1_BONFIRE_STATE[found[bid]]})")
227 (a, len(got), got, len(got) + len(miss), miss)
228 for a, (got, miss)
in areas.items()
236def ds1_augment(ch, data, entries, i, base_dir, dec):
237 if i >= len(entries):
239 buf = dec(data[entries[i].offset : entries[i].offset + entries[i].size])
242 if DS1_MENU_ENTRY < len(entries):
243 e = entries[DS1_MENU_ENTRY]
247 ch[
"bonfire_areas"] = areas
252 attach_defeated_bosses(ch, base_dir)
258DSR_MAGIC = bytes.fromhex(
"00FFFFFFFF000000000000000000000000FFFFFFFF")
262DSR_SOULS_D, DSR_HP_D, DSR_STAM_D, DSR_LEVEL_D, DSR_CLASS_D, DSR_HUM_D = (
272DSR_NG_D, DSR_NAME_D = 0x1E3A7, -271
285DS1_GENDER = {0:
"Female", 1:
"Male"}
297DS1_DEATHS_OFF = {
"ptde": 0x1F118,
"dsr": 0x1F2D8}
300DS1_DEATHS_SENTINEL, DS1_DEATHS_SENTINEL_D = 0xFFFFFFFF, 4
314DS1_MENU_LEVEL_D, DS1_MENU_PLAYTIME_D = 36, 40
326DS1_SLOT_BREAKS = (10, 12, 14, 16, 19, 23, 28, 34, 41, 50)
330DSR_STAT_D = OrderedDict(
333 (
"Attunement", -367),
337 (
"Resistance", -303),
338 (
"Intelligence", -335),
361 0x00000000:
"weapons",
362 0x10000000:
"armors",
370DS1_INV_START, DS1_INV_ANCHOR = 0x988, bytes.fromhex(
"0000000000000000A0BB0D00")
374DS1_INV_END = bytes.fromhex(
"00000000FFFFFFFFFFFFFFFF")
401 table = item_db.get(cat, {})
406 return table.get(iid // 1000)
407 if cat
not in (
"weapons",
"armors"):
409 base, path, level = iid - iid % 1000, (iid % 1000) // 100, iid % 100
410 name = table.get(base)
413 infusion = DS1_INFUSION.get(path)
if cat ==
"weapons" else None
414 suffix = f
" +{level}" if level
else ""
415 return f
"{name}{suffix} ({infusion})" if infusion
else f
"{name}{suffix}"
429 m = buf.find(DSR_MAGIC, o)
432 lvl = u16(buf, m + DSR_LEVEL_D)
433 stats = [u8(buf, m + d)
for d
in DSR_STAT_D.values()]
437 and all(v
is not None and 0 <= v <= 99
for v
in stats)
447 buckets, unknown = defaultdict(list), 0
448 start = buf.find(DS1_INV_ANCHOR, DS1_INV_START)
450 return buckets, unknown
451 end = buf.find(DS1_INV_END, start)
456 stype, iid, qty = u32(buf, o + 4), u32(buf, o + 8), u32(buf, o + 12)
460 cat = DS1_CAT.get(stype & 0xF0000000)
if stype
is not None else None
463 if cat ==
"goods" and iid
in item_db.get(
"spells", {}):
465 name =
ds1_resolve(item_db, cat, iid)
if cat
else None
469 buckets[cat].append((name, qty))
470 return buckets, unknown
482 off = DS1_DEATHS_OFF.get(game)
485 if u32(buf, off + DS1_DEATHS_SENTINEL_D) != DS1_DEATHS_SENTINEL:
499 if not menu
or not ch.get(
"name")
or ch.get(
"level")
is None:
501 want = ch[
"name"].encode(
"utf-16-le")
502 pos = menu.find(want)
504 if u32(menu, pos + DS1_MENU_LEVEL_D) == ch[
"level"]:
505 ch[
"play_time"] = u32(menu, pos + DS1_MENU_PLAYTIME_D)
507 pos = menu.find(want, pos + 2)
520 stats = OrderedDict((k, u8(buf, m + d))
for k, d
in DSR_STAT_D.items())
522 inv = {c: merge_qty(v)
for c, v
in buckets.items()}
523 name = read_utf16(buf, m + DSR_NAME_D, 13)
527 "name": name
if is_valid_name(name)
else "(unnamed slot)",
528 "klass": DS1_CLASS.get(u8(buf, m + DSR_CLASS_D)),
529 "gender": DS1_GENDER.get(u8(buf, m + DSR_GENDER_D)),
531 "level": u16(buf, m + DSR_LEVEL_D),
533 "souls": u32(buf, m + DSR_SOULS_D),
535 "humanity": u8(buf, m + DSR_HUM_D),
536 "stamina": u32(buf, m + DSR_STAM_D),
537 "hp": u32(buf, m + DSR_HP_D),
539 "boss_souls": find_boss_souls(inv.get(
"goods", [])),
540 "key_items": find_key_goods(inv.get(
"goods", [])),
542 "unknown_count": unknown,
547def dsr_parse(buf, item_db):
551 return ds1_character(buf, item_db, m,
"dsr", u8(buf, m + DSR_NG_D)
or 0)
566 o, n = 0, len(buf) - 1
568 name = read_utf16(buf, o, 13)
569 if len(name) >= 2
and is_valid_name(name):
571 lvl = u16(buf, m + DSR_LEVEL_D)
572 stats = [u8(buf, m + d)
for d
in DSR_STAT_D.values()]
576 and all(v
is not None and 0 <= v <= 99
for v
in stats)
587def ptde_parse(buf, item_db):
ds1_attach_flags(ch, buf, base_dir, game)
Merge DS1 boss-defeat FLAGS into ch["bosses"] as flag evidence.
ptde_find_anchor(buf)
Find the PtDE stat anchor (full tier).
ds1_character(buf, item_db, m, game, ng)
Build the unified full-tier dict from a located DS1 stat anchor.
ds1_deaths(buf, game)
Total deaths for a DS1 slot, or None when the struct isn't where expected.
load_ds1_bonfires(base_dir)
ds1_resolve(item_db, cat, iid)
Resolve a DS1 item id to a display name, unwrapping any upgrade baked in.
ds1_bonfires(buf, db)
DS1 bonfires the character has found, grouped by area.
load_ds1_world_events(base_dir)
ds1_inventory(buf, item_db)
Sort the DS1 inventory into categories.
dsr_find_anchor(buf)
Find the true DSR stat anchor.
ds1_attach_playtime(ch, menu)
Attach play time from DS1's load-screen roster block.
load_ds1_known_flags(base_dir)