From 6ca03cc19c13566a1240ff62a6053a9800cb996d Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 22 Oct 2023 15:21:11 +0200 Subject: [PATCH] Add enchantment parsing --- d2warehouse/data/affixes.json | 2 +- d2warehouse/item.py | 36 +++++++-- d2warehouse/parser.py | 133 +++++++++++++++++++++++++++++----- 3 files changed, 148 insertions(+), 23 deletions(-) diff --git a/d2warehouse/data/affixes.json b/d2warehouse/data/affixes.json index fa4e447..5cc3513 100644 --- a/d2warehouse/data/affixes.json +++ b/d2warehouse/data/affixes.json @@ -30,7 +30,7 @@ "40": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Fire Resist"}, "41": {"bits": [8], "bias": 50, "text": "Lightning Resist +{0}%"}, "42": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Lightning Resist"}, - "43": {"bits": [8], "bias": 50, "text": "Cold Resist +{0}%"}, + "43": {"bits": [9], "bias": 200, "text": "Cold Resist +{0}%"}, "44": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Cold Resist"}, "45": {"bits": [8], "bias": 50, "text": "Poison Resist +{0}%"}, "46": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Poison Resist"}, diff --git a/d2warehouse/item.py b/d2warehouse/item.py index 5d118e4..d1ba394 100644 --- a/d2warehouse/item.py +++ b/d2warehouse/item.py @@ -6,6 +6,7 @@ from enum import Enum _data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") _basetype_map = None +_affix_map = None class Quality(Enum): @@ -35,14 +36,20 @@ class LowQualityType(Enum): @dataclass class Affix: name_id: int - id: int | None = None # TODO: read this - # TODO: data + stat_id: int | None = None # TODO: These 3 should probably not be optional + stat_values: list[int] | None = None + stat_text: str | None = None def print(self, indent=5): # TODO: name lookup - # TODO: modified lookup - # TODO: adding in data - print(" " * indent, f"{self.name_id} {hex(self.name_id)}") + if self.stat_text: + subst_text = self.stat_text + for i, val in enumerate(self.stat_values): + replace = "{" + str(i) + "}" + subst_text = subst_text.replace(replace, str(val)) + else: + subst_text = "" + print(" " * indent, f"{hex(self.name_id)}: {subst_text}") def txtbits(bits: bitarray) -> str: @@ -121,6 +128,17 @@ class Item: print(" " * indent, f"Runeword Id: {self.runeword_id}") # TODO: name lookup if self.personal_name: print(" " * indent, f"Personal name: {self.personal_name}") + if self.defense: + print(" " * indent, f"Defense: {self.defense}") + if self.durability is not None: + print( + " " * indent, + f"Durability: {self.durability} out of {self.max_durability}", + ) + if self.sockets: + print(" " * indent, f"Num Sockets: {self.sockets}") + if self.quantity: + print(" " * indent, f"Quantity: {self.quantity}") if with_raw: print(" " * indent, "Raw Item Data:") bits = bitarray(endian="little") @@ -136,3 +154,11 @@ def lookup_basetype(code: str) -> dict: with open(os.path.join(_data_path, "items.json")) as f: _basetype_map = json.load(f) return _basetype_map[code] + + +def lookup_affix(code: int) -> dict: + global _affix_map + if _affix_map is None: + with open(os.path.join(_data_path, "affixes.json")) as f: + _affix_map = json.load(f) + return _affix_map[str(code)] diff --git a/d2warehouse/parser.py b/d2warehouse/parser.py index 818de55..ab43fe8 100644 --- a/d2warehouse/parser.py +++ b/d2warehouse/parser.py @@ -2,7 +2,14 @@ import struct from bitarray import bitarray from bitarray.util import ba2int from d2warehouse.stash import Stash, StashTab -from d2warehouse.item import Affix, Item, LowQualityType, Quality, lookup_basetype +from d2warehouse.item import ( + Affix, + Item, + LowQualityType, + Quality, + lookup_affix, + lookup_basetype, +) import d2warehouse.huffman as huffman STASH_TAB_MAGIC = b"\x55\xAA\x55\xAA" @@ -138,12 +145,12 @@ def parse_item(data: bytes) -> tuple[bytes, Item]: item.inherent, inherent_end = parse_inherent_mod(bits[graphic_end:]) inherent_end += graphic_end - print("in", inherent_end) item, quality_end = parse_quality_data(bits[inherent_end:], item) quality_end += inherent_end if item.is_runeword: item.runeword_id, runeword_end = parse_runeword(bits[quality_end:], item) + runeword_end += quality_end else: runeword_end = quality_end @@ -151,12 +158,17 @@ def parse_item(data: bytes) -> tuple[bytes, Item]: item.personal_name, personalized_end = parse_personalization( bits[runeword_end:], item ) + personalized_end += runeword_end else: personalized_end = runeword_end - item, itemtype_end = parse_basetype_data(bits[personalized_end:], item) + item, itembase_end = parse_basetype_data(bits[personalized_end:], item) + itembase_end += personalized_end - extended_byte_size = int((itemtype_end + 7) / 8) + item, enchantments_end = parse_enchantments(bits[itembase_end:], item) + enchantments_end += itembase_end + + extended_byte_size = int((enchantments_end + 7) / 8) print("extended size", extended_byte_size) item.raw_data = data[:] @@ -271,28 +283,115 @@ def parse_basetype_data(bits: bitarray, item: Item) -> tuple[Item, int]: cls = lookup_basetype(item.kind)["class"] ptr = 0 - if cls == "armor": - item.defense = ba2int(bits[0:10]) + 10 - ptr += 10 - - if cls in ["armor", "weapon"]: - item.max_durability = ba2int(bits[ptr : ptr + 8]) - item.durability = ba2int(bits[ptr : ptr + 8]) - ptr += 16 - if item.is_socketed: - item.sockets = ba2int(bits[ptr : ptr + 4]) - ptr += 4 - if cls == "tome": ptr += 5 # unknown field + # Unknown bit here, supposedly 96 bits of realm data?? follow if set + if bits[ptr : ptr + 1]: + print("Unknown bit set, realm data follows?") + # ptr += 96 -- doesnt seem right + ptr += 1 + + if cls == "armor": + item.defense = ba2int(bits[ptr : ptr + 11]) - 10 + ptr += 11 + + if cls in ["armor", "weapon"]: + item.max_durability = ba2int(bits[ptr : ptr + 8]) + ptr += 8 + if item.max_durability > 0: + item.durability = ba2int(bits[ptr : ptr + 8]) + ptr += 8 + 1 # uknown bit after durability + if cls in ["tome", "stackable"]: - item.quality = ba2int(bits[ptr : ptr + 9]) + item.quantity = ba2int(bits[ptr : ptr + 9]) ptr += 9 + if item.is_socketed: + item.sockets = ba2int(bits[ptr : ptr + 4]) + ptr += 4 + return item, ptr +def parse_enchantments(bits: bitarray, item: Item) -> tuple[Item, int]: + ptr = 0 + if item.quality == Quality.SET: + val = ba2int(bits[0:5]) + # credit to https://github.com/nokka/d2s/blob/426ae713940b7474a5f7872f16dddb02ced8a241/item.go#L1139 + # for table + table = { + 0: 0, + 1: 1, + 2: 1, + 3: 2, + 4: 1, + 6: 2, + 7: 3, + 10: 2, + 12: 2, + 15: 4, + 31: 5, + } + num_lists = 1 + table[val] + ptr += 5 + else: + num_lists = 1 + + while num_lists > 0: + affixes, ptr = parse_enchantment_list(bits[ptr:]) + num_lists -= 1 + + for i, affix in enumerate(affixes): + # TODO: this works for magic, rare & crafted. But set & unique items do not have + # names specified in the save data, but do instead need to be read from data files + # to know if the mod is prefix or suffix. For now we add them all to prefixes. + # TODO: Also note, that sets have lists of mods they get from the set. Which should go + # seperately (assuming they are in the data files when not equipped...?) + if item.quality in [Quality.UNIQUE, Quality.SET]: + item.prefixes.append(Affix(name_id=0)) + mod = item.prefixes[-1] + else: + # TODO: Here we assume that they are the same ordering as previously + mod = ( + item.prefixes[i] + if i < len(item.prefixes) + else item.suffixes[i - len(item.prefixes)] + ) + + mod.stat_id = affix.stat_id + mod.stat_values = affix.stat_values + mod.stat_text = affix.stat_text + + return item, ptr + + +def parse_enchantment_list(bits: bitarray) -> tuple[list[Affix], int]: + ptr = 0 # what is all this data? + affixes = [] + + while True: + id = ba2int(bits[ptr : ptr + 9]) + ptr += 9 + if id == 0x1FF: + break + print("id", id) + + affix = lookup_affix(id) + + values = [] + for b in affix["bits"]: + print("bits", b) + values.append(ba2int(bits[ptr : ptr + b])) + ptr += b + values = [v - affix["bias"] for v in values] + text = affix["text"] + + affixes.append(Affix(name_id=0, stat_id=id, stat_values=values, stat_text=text)) + + return affixes, ptr + + def parse_items(data: bytes) -> list[Item]: data = parse_fixed(data, ITEM_DATA_MAGIC) data, num = parse_u16(data)