Add parsing of quality specific data

This commit is contained in:
2023-10-21 19:57:02 +02:00
parent a741aa6ff0
commit f9f71e1185
3 changed files with 187 additions and 54 deletions

View File

@@ -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)

View File

@@ -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("")

View File

@@ -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]: