From f9f71e1185dbd63ffb56dec3b547ad87186ea6c6 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 21 Oct 2023 19:57:02 +0200 Subject: [PATCH] Add parsing of quality specific data --- d2warehouse/dump.py | 10 +-- d2warehouse/item.py | 70 +++++++++++++++--- d2warehouse/parser.py | 161 +++++++++++++++++++++++++++++++----------- 3 files changed, 187 insertions(+), 54 deletions(-) diff --git a/d2warehouse/dump.py b/d2warehouse/dump.py index d43e07e..a960512 100644 --- a/d2warehouse/dump.py +++ b/d2warehouse/dump.py @@ -11,7 +11,7 @@ def txtbits(bits: bitarray) -> str: return " ".join(grouped) -def read_stash(path): +def read_stash(path, verbose): with path.open("rb") as f: stash = parse_stash(f.read()) @@ -19,9 +19,10 @@ def read_stash(path): for i, tab in enumerate(stash.tabs): print(f" - tab {i}") print(f" - {len(tab.items)} items") - print(f" - {tab.item_data.hex()}") + if verbose: + print(f" - {tab.item_data.hex()}") for j, item in enumerate(tab.items): - item.print() + item.print(with_raw=verbose) def main(): @@ -30,5 +31,6 @@ def main(): description="dump a text description of the items in d2r shared stash files", ) parser.add_argument("stashfile", type=Path) + parser.add_argument("-v", "--verbose", action="store_true") args = parser.parse_args() - read_stash(args.stashfile) + read_stash(args.stashfile, args.verbose) diff --git a/d2warehouse/item.py b/d2warehouse/item.py index 63abaae..c10bf67 100644 --- a/d2warehouse/item.py +++ b/d2warehouse/item.py @@ -17,6 +17,29 @@ class Quality(Enum): return self.name.capitalize() +class LowQualityType(Enum): + CRUDE = 0 + CRACKED = 1 + DAMAGED = 2 + LOW_QUALITY = 3 + + def __str__(self) -> str: + return self.name.capitalize() + + +@dataclass +class Affix: + name_id: int + id: int | None = None # TODO: read this + # TODO: data + + 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)}") + + def txtbits(bits: bitarray) -> str: txt = "".join(str(b) for b in bits) grouped = [txt[i : i + 8] for i in range(0, len(txt), 8)] @@ -36,13 +59,22 @@ class Item: pos_x: int pos_y: int kind: str - uid: int | None - lvl: int | None - quality: Quality | None - graphic: int | None - inherent: int | None + uid: int | None = None + lvl: int | None = None + quality: Quality | None = None + graphic: int | None = None + inherent: int | None = None + low_quality: LowQualityType | None = None + prefixes: list[Affix] | None = None + suffixes: list[Affix] | None = None + set_id: int | None = None + unique_id: int | None = None + nameword1: int | None = None + nameword2: int | None = None + runeword_id: int | None = None + personal_name: str | None = None - def print(self, indent=5): + def print(self, indent=5, with_raw=False): properties = [] print(" " * indent, self.kind) if self.lvl: @@ -60,8 +92,28 @@ class Item: if properties: print(" " * indent, ", ".join(properties)) print(" " * indent, f"at {self.pos_x}, {self.pos_y}") - bits = bitarray(endian="little") - bits.frombytes(self.raw_data) - print(" " * indent, txtbits(bits)) + if self.quality: + print(" " * indent, self.quality) + if self.prefixes: + print(" " * indent, "Prefixes:") + for prefix in self.prefixes: + prefix.print(indent + 4) + if self.suffixes: + print(" " * indent, "Suffixes:") + for suffix in self.suffixes: + suffix.print(indent + 4) + if self.set_id: + print(" " * indent, f"Set Id: {self.set_id}") # TODO: name lookup + if self.unique_id: + print(" " * indent, f"Set Id: {self.unique_id}") # TODO: name lookup + if self.runeword_id: + print(" " * indent, f"Runeword Id: {self.runeword_id}") # TODO: name lookup + if self.personal_name: + print(" " * indent, f"Personal name: {self.personal_name}") + if with_raw: + print(" " * indent, "Raw Item Data:") + bits = bitarray(endian="little") + bits.frombytes(self.raw_data) + print(" " * indent, txtbits(bits)) print("") print("") diff --git a/d2warehouse/parser.py b/d2warehouse/parser.py index 63fe7f6..294f2f8 100644 --- a/d2warehouse/parser.py +++ b/d2warehouse/parser.py @@ -2,7 +2,7 @@ import struct from bitarray import bitarray from bitarray.util import ba2int from d2warehouse.stash import Stash, StashTab -from d2warehouse.item import Item, Quality +from d2warehouse.item import Affix, Item, LowQualityType, Quality import d2warehouse.huffman as huffman STASH_TAB_MAGIC = b"\x55\xAA\x55\xAA" @@ -110,20 +110,10 @@ def parse_item(data: bytes) -> tuple[bytes, Item]: if is_ear: raise UnsupportedItemError("Ear items are not supported") - uid, lvl, quality, graphic, inherent = None, None, None, None, None - if not is_simple: - uid, lvl, quality = parse_extended_item(bits[sockets_end:]) - - extended_end = sockets_end + 32 + 7 + 4 - - graphic, graphic_end = parse_item_graphic(bits[extended_end:]) - graphic_end += extended_end - - inherent, inherent_end = parse_inherent_mod(bits[graphic_end:]) - inherent_end += graphic_end - + simple_byte_sz = int((sockets_end + 7) / 8) + print("simple size", simple_byte_sz) item = Item( - data[:], # TODO: only take parsed bytes + data[:simple_byte_sz], is_identified, is_socketed, is_beginner, @@ -134,24 +124,52 @@ def parse_item(data: bytes) -> tuple[bytes, Item]: pos_x, pos_y, kind, - uid, - lvl, - quality, - graphic, - inherent, ) + if is_simple: + return data[simple_byte_sz:], item + + item.uid, item.lvl, item.quality = parse_extended_item(bits[sockets_end:]) + + extended_end = sockets_end + 32 + 7 + 4 + + item.graphic, graphic_end = parse_item_graphic(bits[extended_end:]) + graphic_end += extended_end + + 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) + else: + runeword_end = quality_end + + if item.is_personalized: + item.personal_name, personalized_end = parse_personalization( + bits[runeword_end:], item + ) + else: + personalized_end = runeword_end + + extended_byte_size = int((personalized_end + 7) / 8) + print("extended size", extended_byte_size) + + item.raw_data = data[:] return b"", item # TODO: properly return remaining data def parse_item_graphic(bits: bitarray) -> tuple[int | None, int]: - if bits[0]: + if not bits[0]: return None, 1 else: return ba2int(bits[1:4]), 4 def parse_inherent_mod(bits: bitarray) -> tuple[int | None, int]: - if bits[0]: + if not bits[0]: return None, 1 else: return ba2int(bits[1:4]), 4 @@ -164,26 +182,87 @@ def parse_extended_item(bits: bitarray) -> tuple[int, int, Quality]: return uid, lvl, Quality(quality) -# TODO: figure out what we want to return here -# TODO: Introduce enums for quality -def parse_quality_data(bits: bitarray, quality: int): - if quality == 1: - return parse_low_quality_data(bits) - elif quality == 2: - return None, 0 - elif quality == 3: - return parse_high_quality_data(bits) - elif quality == 4: - return parse_magic_data(bits) - elif quality == 5: - return parse_set_data(bits) - elif quality == 6: - return parse_rare_data(bits) - elif quality == 7: - return parse_unique_data(bits) - elif quality == 8: - return parse_crafted_data(bits) - # magic +def parse_quality_data(bits: bitarray, item: Item) -> tuple[Item, int]: + if item.quality == Quality.LOW: + return parse_low_quality_data(bits, item) + elif item.quality == item.quality.NORMAL: + return item, 0 + elif item.quality == item.quality.HIGH: + return parse_high_quality_data(bits, item) + elif item.quality == item.quality.MAGIC: + return parse_magic_data(bits, item) + elif item.quality == item.quality.SET: + return parse_set_data(bits, item) + elif item.quality == item.quality.RARE: + return parse_rare_data(bits, item) + elif item.quality == item.quality.UNIQUE: + return parse_unique_data(bits, item) + elif item.quality == item.quality.CRAFTED: + return parse_rare_data(bits, item) # crafted & rare are the same + + +def parse_low_quality_data(bits: bitarray, item: Item) -> tuple[Item, int]: + item.low_quality = LowQualityType(ba2int(bits[0:3])) + return item, 3 + + +def parse_high_quality_data(bits: bitarray, item: Item) -> tuple[Item, int]: + # The data for superior item is unknown + return item, 3 + + +def parse_magic_data(bits: bitarray, item: Item) -> tuple[Item, int]: + item.prefixes = [Affix(name_id=ba2int(bits[0:11]))] + item.suffixes = [Affix(name_id=ba2int(bits[11:22]))] + return item, 22 + + +def parse_set_data(bits: bitarray, item: Item) -> tuple[Item, int]: + item.set_id = ba2int(bits[0:12]) + return item, 12 + + +def parse_rare_data(bits: bitarray, item: Item) -> tuple[Item, int]: + item.nameword1 = ba2int(bits[0:8]) + item.nameword2 = ba2int(bits[8:16]) + affixes = [] + ptr = 16 + for _ in range(0, 6): + (affix, sz) = parse_affix(bits[ptr:]) + ptr += sz + affixes.append(affix) + item.prefixes = [affix for affix in affixes[0:3] if affix is not None] + item.suffixes = [affix for affix in affixes[3:6] if affix is not None] + + return item, ptr + + +def parse_unique_data(bits: bitarray, item: Item) -> tuple[Item, int]: + item.unique_id = ba2int(bits[0:12]) + return item, 12 + + +def parse_affix(bits: bitarray) -> tuple[Affix | None, int]: + if not bits[0]: + return None, 1 + else: + return Affix(name_id=ba2int(bits[1:12])), 12 + + +def parse_runeword(bits: bitarray) -> tuple[int, int]: + id = ba2int(bits[0:12]) + return id, 16 + + +def parse_personalization(bits: bitarray) -> tuple[str, int]: + output = "" + ptr = 0 + ascii = ba2int(bits[0:7]) + while ascii: + output += chr(ascii) + ascii = ba2int(bits[ptr : ptr + 7]) + ptr += 7 + return output, ptr + 1 def parse_items(data: bytes) -> list[Item]: