Add enchantment parsing
This commit is contained in:
@@ -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"},
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user