Add enchantment parsing

This commit is contained in:
2023-10-22 15:21:11 +02:00
parent e99d93a6a0
commit 6ca03cc19c
3 changed files with 148 additions and 23 deletions

View File

@@ -30,7 +30,7 @@
"40": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Fire Resist"}, "40": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Fire Resist"},
"41": {"bits": [8], "bias": 50, "text": "Lightning Resist +{0}%"}, "41": {"bits": [8], "bias": 50, "text": "Lightning Resist +{0}%"},
"42": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Lightning Resist"}, "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"}, "44": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Cold Resist"},
"45": {"bits": [8], "bias": 50, "text": "Poison Resist +{0}%"}, "45": {"bits": [8], "bias": 50, "text": "Poison Resist +{0}%"},
"46": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Poison Resist"}, "46": {"bits": [5], "bias": 0, "text": "+{0}% to Maximum Poison Resist"},

View File

@@ -6,6 +6,7 @@ from enum import Enum
_data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") _data_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
_basetype_map = None _basetype_map = None
_affix_map = None
class Quality(Enum): class Quality(Enum):
@@ -35,14 +36,20 @@ class LowQualityType(Enum):
@dataclass @dataclass
class Affix: class Affix:
name_id: int name_id: int
id: int | None = None # TODO: read this stat_id: int | None = None # TODO: These 3 should probably not be optional
# TODO: data stat_values: list[int] | None = None
stat_text: str | None = None
def print(self, indent=5): def print(self, indent=5):
# TODO: name lookup # TODO: name lookup
# TODO: modified lookup if self.stat_text:
# TODO: adding in data subst_text = self.stat_text
print(" " * indent, f"{self.name_id} {hex(self.name_id)}") for i, val in enumerate(self.stat_values):
replace = "{" + str(i) + "}"
subst_text = subst_text.replace(replace, str(val))
else:
subst_text = "<No text>"
print(" " * indent, f"{hex(self.name_id)}: {subst_text}")
def txtbits(bits: bitarray) -> str: def txtbits(bits: bitarray) -> str:
@@ -121,6 +128,17 @@ class Item:
print(" " * indent, f"Runeword Id: {self.runeword_id}") # TODO: name lookup print(" " * indent, f"Runeword Id: {self.runeword_id}") # TODO: name lookup
if self.personal_name: if self.personal_name:
print(" " * indent, f"Personal name: {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: if with_raw:
print(" " * indent, "Raw Item Data:") print(" " * indent, "Raw Item Data:")
bits = bitarray(endian="little") 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: with open(os.path.join(_data_path, "items.json")) as f:
_basetype_map = json.load(f) _basetype_map = json.load(f)
return _basetype_map[code] 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)]

View File

@@ -2,7 +2,14 @@ import struct
from bitarray import bitarray from bitarray import bitarray
from bitarray.util import ba2int from bitarray.util import ba2int
from d2warehouse.stash import Stash, StashTab 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 import d2warehouse.huffman as huffman
STASH_TAB_MAGIC = b"\x55\xAA\x55\xAA" 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:]) item.inherent, inherent_end = parse_inherent_mod(bits[graphic_end:])
inherent_end += graphic_end inherent_end += graphic_end
print("in", inherent_end)
item, quality_end = parse_quality_data(bits[inherent_end:], item) item, quality_end = parse_quality_data(bits[inherent_end:], item)
quality_end += inherent_end quality_end += inherent_end
if item.is_runeword: if item.is_runeword:
item.runeword_id, runeword_end = parse_runeword(bits[quality_end:], item) item.runeword_id, runeword_end = parse_runeword(bits[quality_end:], item)
runeword_end += quality_end
else: else:
runeword_end = quality_end runeword_end = quality_end
@@ -151,12 +158,17 @@ def parse_item(data: bytes) -> tuple[bytes, Item]:
item.personal_name, personalized_end = parse_personalization( item.personal_name, personalized_end = parse_personalization(
bits[runeword_end:], item bits[runeword_end:], item
) )
personalized_end += runeword_end
else: else:
personalized_end = runeword_end 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) print("extended size", extended_byte_size)
item.raw_data = data[:] 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"] cls = lookup_basetype(item.kind)["class"]
ptr = 0 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": if cls == "tome":
ptr += 5 # unknown field 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"]: if cls in ["tome", "stackable"]:
item.quality = ba2int(bits[ptr : ptr + 9]) item.quantity = ba2int(bits[ptr : ptr + 9])
ptr += 9 ptr += 9
if item.is_socketed:
item.sockets = ba2int(bits[ptr : ptr + 4])
ptr += 4
return item, ptr 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]: def parse_items(data: bytes) -> list[Item]:
data = parse_fixed(data, ITEM_DATA_MAGIC) data = parse_fixed(data, ITEM_DATA_MAGIC)
data, num = parse_u16(data) data, num = parse_u16(data)