From c170241746414bc5f5915113b555ec2ba79c790a Mon Sep 17 00:00:00 2001 From: Andreas Date: Tue, 24 Oct 2023 20:26:21 +0200 Subject: [PATCH] Add runeword parsing --- d2warehouse/contrib/runewords.py | 27 + d2warehouse/data/runewords.json | 1120 ++++++++++++++++++++++++++ d2warehouse/item.py | 14 +- d2warehouse/parser.py | 6 +- d2warehouse/tests/test_parse_item.py | 21 +- 5 files changed, 1185 insertions(+), 3 deletions(-) create mode 100644 d2warehouse/contrib/runewords.py create mode 100644 d2warehouse/data/runewords.json diff --git a/d2warehouse/contrib/runewords.py b/d2warehouse/contrib/runewords.py new file mode 100644 index 0000000..be29311 --- /dev/null +++ b/d2warehouse/contrib/runewords.py @@ -0,0 +1,27 @@ +import csv +import json +import os +import sys + + +path = sys.argv[1] if len(sys.argv) >= 2 else "." + +runewords = {} +id = 0 +with open(os.path.join(path, "runes.txt")) as f: + dr = csv.DictReader(f, delimiter="\t") + for row in dr: + id += 1 + if row["complete"] != "1": + continue + runewords[id + 26] = { + "name": row["*Rune Name"], + "itembases": [ + row[f"itype{i}"] for i in range(1, 7) if len(row[f"itype{i}"]) > 0 + ], + "runes": [row[f"Rune{i}"] for i in range(1, 7) if len(row[f"Rune{i}"]) > 0], + } + +with open("runewords.json", "w", newline="\n") as f: + json.dump(runewords, f, indent=4) + f.write("\n") diff --git a/d2warehouse/data/runewords.json b/d2warehouse/data/runewords.json new file mode 100644 index 0000000..702e12a --- /dev/null +++ b/d2warehouse/data/runewords.json @@ -0,0 +1,1120 @@ +{ + "27": { + "name": "Ancients' Pledge", + "itembases": [ + "shld" + ], + "runes": [ + "r08", + "r09", + "r07" + ] + }, + "30": { + "name": "Beast", + "itembases": [ + "axe", + "scep", + "hamm" + ], + "runes": [ + "r30", + "r03", + "r22", + "r23", + "r17" + ] + }, + "32": { + "name": "Black", + "itembases": [ + "club", + "hamm", + "mace" + ], + "runes": [ + "r10", + "r16", + "r04" + ] + }, + "34": { + "name": "Bone", + "itembases": [ + "tors" + ], + "runes": [ + "r12", + "r22", + "r22" + ] + }, + "35": { + "name": "Bramble", + "itembases": [ + "tors" + ], + "runes": [ + "r08", + "r27", + "r29", + "r05" + ] + }, + "36": { + "name": "Brand", + "itembases": [ + "miss" + ], + "runes": [ + "r31", + "r28", + "r23", + "r25" + ] + }, + "37": { + "name": "Breath of the Dying", + "itembases": [ + "weap" + ], + "runes": [ + "r26", + "r15", + "r01", + "r02", + "r33", + "r05" + ] + }, + "39": { + "name": "Call to Arms", + "itembases": [ + "weap" + ], + "runes": [ + "r11", + "r08", + "r23", + "r24", + "r27" + ] + }, + "40": { + "name": "Chains of Honor", + "itembases": [ + "tors" + ], + "runes": [ + "r14", + "r22", + "r30", + "r24" + ] + }, + "42": { + "name": "Chaos", + "itembases": [ + "h2h" + ], + "runes": [ + "r19", + "r27", + "r22" + ] + }, + "43": { + "name": "Crescent Moon", + "itembases": [ + "axe", + "swor", + "pole" + ], + "runes": [ + "r13", + "r22", + "r03" + ] + }, + "46": { + "name": "Death", + "itembases": [ + "swor", + "axe" + ], + "runes": [ + "r15", + "r01", + "r26", + "r09", + "r25" + ] + }, + "48": { + "name": "Delirium", + "itembases": [ + "helm" + ], + "runes": [ + "r20", + "r24", + "r16" + ] + }, + "51": { + "name": "Destruction", + "itembases": [ + "pole", + "swor" + ], + "runes": [ + "r26", + "r28", + "r30", + "r31", + "r18" + ] + }, + "52": { + "name": "Doom", + "itembases": [ + "axe", + "pole", + "hamm" + ], + "runes": [ + "r15", + "r27", + "r22", + "r28", + "r32" + ] + }, + "53": { + "name": "Dragon", + "itembases": [ + "tors", + "shld" + ], + "runes": [ + "r29", + "r28", + "r12" + ] + }, + "55": { + "name": "Dream", + "itembases": [ + "helm", + "shld" + ], + "runes": [ + "r16", + "r31", + "r21" + ] + }, + "56": { + "name": "Duress", + "itembases": [ + "tors" + ], + "runes": [ + "r13", + "r22", + "r10" + ] + }, + "57": { + "name": "Edge", + "itembases": [ + "miss" + ], + "runes": [ + "r03", + "r07", + "r11" + ] + }, + "59": { + "name": "Enigma", + "itembases": [ + "tors" + ], + "runes": [ + "r31", + "r06", + "r30" + ] + }, + "60": { + "name": "Enlightenment", + "itembases": [ + "tors" + ], + "runes": [ + "r21", + "r08", + "r12" + ] + }, + "62": { + "name": "Eternity", + "itembases": [ + "mele" + ], + "runes": [ + "r11", + "r30", + "r24", + "r12", + "r29" + ] + }, + "63": { + "name": "Exile", + "itembases": [ + "pala" + ], + "runes": [ + "r26", + "r27", + "r24", + "r14" + ] + }, + "64": { + "name": "Faith", + "itembases": [ + "miss" + ], + "runes": [ + "r27", + "r31", + "r20", + "r02" + ] + }, + "65": { + "name": "Famine", + "itembases": [ + "axe", + "hamm" + ], + "runes": [ + "r19", + "r27", + "r09", + "r31" + ] + }, + "66": { + "name": "Flickering Flame", + "itembases": [ + "helm" + ], + "runes": [ + "r04", + "r21", + "r26" + ] + }, + "67": { + "name": "Fortitude", + "itembases": [ + "weap", + "tors" + ], + "runes": [ + "r01", + "r12", + "r14", + "r28" + ] + }, + "70": { + "name": "Fury", + "itembases": [ + "mele" + ], + "runes": [ + "r31", + "r25", + "r05" + ] + }, + "71": { + "name": "Gloom", + "itembases": [ + "tors" + ], + "runes": [ + "r19", + "r22", + "r21" + ] + }, + "73": { + "name": "Grief", + "itembases": [ + "swor", + "axe" + ], + "runes": [ + "r05", + "r03", + "r28", + "r23", + "r08" + ] + }, + "74": { + "name": "Hand of Justice", + "itembases": [ + "weap" + ], + "runes": [ + "r29", + "r32", + "r11", + "r28" + ] + }, + "75": { + "name": "Harmony", + "itembases": [ + "miss" + ], + "runes": [ + "r03", + "r06", + "r12", + "r18" + ] + }, + "77": { + "name": "Heart of the Oak", + "itembases": [ + "staf", + "mace" + ], + "runes": [ + "r18", + "r26", + "r21", + "r10" + ] + }, + "80": { + "name": "Holy Thunder", + "itembases": [ + "scep" + ], + "runes": [ + "r05", + "r08", + "r09", + "r07" + ] + }, + "81": { + "name": "Honor", + "itembases": [ + "mele" + ], + "runes": [ + "r11", + "r01", + "r06", + "r03", + "r12" + ] + }, + "85": { + "name": "Ice", + "itembases": [ + "miss" + ], + "runes": [ + "r11", + "r13", + "r31", + "r28" + ] + }, + "86": { + "name": "Infinity", + "itembases": [ + "pole", + "spea" + ], + "runes": [ + "r30", + "r23", + "r30", + "r24" + ] + }, + "88": { + "name": "Insight", + "itembases": [ + "pole", + "staf", + "miss" + ], + "runes": [ + "r08", + "r03", + "r07", + "r12" + ] + }, + "91": { + "name": "King's Grace", + "itembases": [ + "swor", + "scep" + ], + "runes": [ + "r11", + "r08", + "r10" + ] + }, + "92": { + "name": "Kingslayer", + "itembases": [ + "swor", + "axe" + ], + "runes": [ + "r23", + "r22", + "r25", + "r19" + ] + }, + "95": { + "name": "Last Wish", + "itembases": [ + "swor", + "hamm", + "axe" + ], + "runes": [ + "r31", + "r23", + "r31", + "r29", + "r31", + "r30" + ] + }, + "97": { + "name": "Lawbringer", + "itembases": [ + "swor", + "hamm", + "scep" + ], + "runes": [ + "r11", + "r20", + "r18" + ] + }, + "98": { + "name": "Leaf", + "itembases": [ + "staf" + ], + "runes": [ + "r03", + "r08" + ] + }, + "100": { + "name": "Lionheart", + "itembases": [ + "tors" + ], + "runes": [ + "r15", + "r17", + "r19" + ] + }, + "101": { + "name": "Lore", + "itembases": [ + "helm" + ], + "runes": [ + "r09", + "r12" + ] + }, + "106": { + "name": "Malice", + "itembases": [ + "mele" + ], + "runes": [ + "r06", + "r01", + "r05" + ] + }, + "107": { + "name": "Melody", + "itembases": [ + "miss" + ], + "runes": [ + "r13", + "r18", + "r04" + ] + }, + "108": { + "name": "Memory", + "itembases": [ + "staf" + ], + "runes": [ + "r17", + "r16", + "r12", + "r05" + ] + }, + "109": { + "name": "Mist", + "itembases": [ + "miss" + ], + "runes": [ + "r32", + "r13", + "r25", + "r10", + "r06" + ] + }, + "112": { + "name": "Myth", + "itembases": [ + "tors" + ], + "runes": [ + "r15", + "r11", + "r04" + ] + }, + "113": { + "name": "Nadir", + "itembases": [ + "helm" + ], + "runes": [ + "r04", + "r03" + ] + }, + "116": { + "name": "Oath", + "itembases": [ + "swor", + "axe", + "mace" + ], + "runes": [ + "r13", + "r21", + "r23", + "r17" + ] + }, + "117": { + "name": "Obedience", + "itembases": [ + "pole", + "spea" + ], + "runes": [ + "r15", + "r18", + "r10", + "r05", + "r19" + ] + }, + "119": { + "name": "Obsession", + "itembases": [ + "staf" + ], + "runes": [ + "r33", + "r24", + "r20", + "r17", + "r16", + "r04" + ] + }, + "120": { + "name": "Passion", + "itembases": [ + "weap" + ], + "runes": [ + "r14", + "r09", + "r02", + "r20" + ] + }, + "122": { + "name": "Pattern", + "itembases": [ + "h2h" + ], + "runes": [ + "r07", + "r09", + "r10" + ] + }, + "123": { + "name": "Peace", + "itembases": [ + "tors" + ], + "runes": [ + "r13", + "r10", + "r11" + ] + }, + "124": { + "name": "Voice of Reason", + "itembases": [ + "swor", + "mace" + ], + "runes": [ + "r20", + "r18", + "r01", + "r02" + ] + }, + "128": { + "name": "Phoenix", + "itembases": [ + "weap", + "shld" + ], + "runes": [ + "r26", + "r26", + "r28", + "r31" + ] + }, + "131": { + "name": "Plague", + "itembases": [ + "swor", + "knif", + "h2h" + ], + "runes": [ + "r32", + "r13", + "r22" + ] + }, + "134": { + "name": "Pride", + "itembases": [ + "pole", + "spea" + ], + "runes": [ + "r32", + "r29", + "r16", + "r28" + ] + }, + "135": { + "name": "Principle", + "itembases": [ + "tors" + ], + "runes": [ + "r08", + "r25", + "r02" + ] + }, + "137": { + "name": "Prudence", + "itembases": [ + "tors" + ], + "runes": [ + "r23", + "r03" + ] + }, + "141": { + "name": "Radiance", + "itembases": [ + "helm" + ], + "runes": [ + "r04", + "r12", + "r06" + ] + }, + "142": { + "name": "Rain", + "itembases": [ + "tors" + ], + "runes": [ + "r09", + "r23", + "r06" + ] + }, + "145": { + "name": "Rhyme", + "itembases": [ + "shld" + ], + "runes": [ + "r13", + "r05" + ] + }, + "146": { + "name": "Rift", + "itembases": [ + "pole", + "scep" + ], + "runes": [ + "r15", + "r18", + "r20", + "r25" + ] + }, + "147": { + "name": "Sanctuary", + "itembases": [ + "shld" + ], + "runes": [ + "r18", + "r18", + "r23" + ] + }, + "151": { + "name": "Silence", + "itembases": [ + "weap" + ], + "runes": [ + "r14", + "r02", + "r15", + "r24", + "r03", + "r26" + ] + }, + "153": { + "name": "Smoke", + "itembases": [ + "tors" + ], + "runes": [ + "r04", + "r17" + ] + }, + "155": { + "name": "Spirit", + "itembases": [ + "swor", + "shld" + ], + "runes": [ + "r07", + "r10", + "r09", + "r11" + ] + }, + "156": { + "name": "Splendor", + "itembases": [ + "shld" + ], + "runes": [ + "r05", + "r17" + ] + }, + "158": { + "name": "Stealth", + "itembases": [ + "tors" + ], + "runes": [ + "r07", + "r05" + ] + }, + "159": { + "name": "Steel", + "itembases": [ + "swor", + "axe", + "mace" + ], + "runes": [ + "r03", + "r01" + ] + }, + "162": { + "name": "Stone", + "itembases": [ + "tors" + ], + "runes": [ + "r13", + "r22", + "r21", + "r17" + ] + }, + "164": { + "name": "Strength", + "itembases": [ + "mele" + ], + "runes": [ + "r11", + "r03" + ] + }, + "173": { + "name": "Treachery", + "itembases": [ + "tors" + ], + "runes": [ + "r13", + "r10", + "r20" + ] + }, + "176": { + "name": "Unbending Will", + "itembases": [ + "swor" + ], + "runes": [ + "r19", + "r16", + "r06", + "r02", + "r01", + "r15" + ] + }, + "179": { + "name": "Venom", + "itembases": [ + "weap" + ], + "runes": [ + "r07", + "r14", + "r23" + ] + }, + "185": { + "name": "Wealth", + "itembases": [ + "tors" + ], + "runes": [ + "r20", + "r18", + "r03" + ] + }, + "187": { + "name": "White", + "itembases": [ + "wand" + ], + "runes": [ + "r14", + "r16" + ] + }, + "188": { + "name": "Wind", + "itembases": [ + "mele" + ], + "runes": [ + "r29", + "r01" + ] + }, + "190": { + "name": "Wisdom", + "itembases": [ + "helm" + ], + "runes": [ + "r21", + "r06", + "r02" + ] + }, + "193": { + "name": "Wrath", + "itembases": [ + "miss" + ], + "runes": [ + "r21", + "r17", + "r30", + "r23" + ] + }, + "195": { + "name": "Zephyr", + "itembases": [ + "miss" + ], + "runes": [ + "r09", + "r05" + ] + }, + "196": { + "name": "Hustle (armor)", + "itembases": [ + "tors" + ], + "runes": [ + "r13", + "r18", + "r02" + ] + }, + "197": { + "name": "Hustle (weapon)", + "itembases": [ + "weap" + ], + "runes": [ + "r13", + "r18", + "r02" + ] + }, + "198": { + "name": "Mosaic", + "itembases": [ + "h2h" + ], + "runes": [ + "r23", + "r25", + "r11" + ] + }, + "199": { + "name": "Metamorphosis", + "itembases": [ + "helm" + ], + "runes": [ + "r16", + "r32", + "r19" + ] + }, + "200": { + "name": "Ground", + "itembases": [ + "helm" + ], + "runes": [ + "r13", + "r16", + "r09" + ] + }, + "201": { + "name": "Temper", + "itembases": [ + "helm" + ], + "runes": [ + "r13", + "r16", + "r08" + ] + }, + "202": { + "name": "Hearth", + "itembases": [ + "helm" + ], + "runes": [ + "r13", + "r16", + "r10" + ] + }, + "203": { + "name": "Cure", + "itembases": [ + "helm" + ], + "runes": [ + "r13", + "r16", + "r07" + ] + }, + "204": { + "name": "Bulwark", + "itembases": [ + "helm" + ], + "runes": [ + "r13", + "r16", + "r12" + ] + } +} diff --git a/d2warehouse/item.py b/d2warehouse/item.py index 31c2a56..fe56233 100644 --- a/d2warehouse/item.py +++ b/d2warehouse/item.py @@ -26,6 +26,7 @@ _basetype_map = None _stats_map = None _unique_map = None _set_item_map = None +_runeword_map = None class Quality(Enum): @@ -127,6 +128,8 @@ class Item: properties.append("Unidentified") if self.is_socketed: properties.append("Socketed") + if self.is_runeword: + properties.append("Runeword") if properties: print(" " * indent, ", ".join(properties)) print(" " * indent, f"at {self.pos_x}, {self.pos_y}") @@ -143,7 +146,8 @@ class Item: itm = lookup_unique(self.unique_id) print(" " * indent, f"{itm['name']} ({self.unique_id})") if self.runeword_id: - print(" " * indent, f"Runeword Id: {self.runeword_id}") # TODO: name lookup + rw = lookup_runeword(self.runeword_id) + print(" " * indent, f"{rw['name']} runeword") if self.personal_name: print(" " * indent, f"Personal name: {self.personal_name}") if self.defense: @@ -200,3 +204,11 @@ def lookup_set_item(id: int) -> dict: with open(os.path.join(_data_path, "sets.json")) as f: _set_item_map = json.load(f) return _set_item_map[str(id)] + + +def lookup_runeword(id: int) -> dict: + global _runeword_map + if _runeword_map is None: + with open(os.path.join(_data_path, "runewords.json")) as f: + _runeword_map = json.load(f) + return _runeword_map[str(id)] diff --git a/d2warehouse/parser.py b/d2warehouse/parser.py index 44cd33b..2440f5f 100644 --- a/d2warehouse/parser.py +++ b/d2warehouse/parser.py @@ -164,7 +164,7 @@ def parse_item(data: bytes) -> tuple[bytes, Item]: quality_end += inherent_end 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:]) runeword_end += quality_end else: runeword_end = quality_end @@ -279,6 +279,7 @@ def parse_affix(bits: bitarray) -> tuple[int | None, int]: def parse_runeword(bits: bitarray) -> tuple[int, int]: id = ba2int(bits[0:12]) + # + 4 unknown bits return id, 16 @@ -346,6 +347,9 @@ def parse_enchantments(bits: bitarray, item: Item) -> tuple[Item, int]: } num_lists = 1 + table[val] ptr += 5 + elif item.is_runeword: + # Runewords have one empty dummy list of only 0x1ff followed by the runeword stats + num_lists = 2 else: num_lists = 1 diff --git a/d2warehouse/tests/test_parse_item.py b/d2warehouse/tests/test_parse_item.py index c6e888b..279d3f7 100644 --- a/d2warehouse/tests/test_parse_item.py +++ b/d2warehouse/tests/test_parse_item.py @@ -1,6 +1,6 @@ import unittest from d2warehouse.parser import parse_item -from d2warehouse.item import Quality +from d2warehouse.item import Quality, lookup_runeword class ParseItemTest(unittest.TestCase): @@ -80,3 +80,22 @@ class ParseItemTest(unittest.TestCase): self.assertEqual(item.code, "spr") self.assertEqual(str(item.stats[0]), "+13% Enhanced Damage") self.assertEqual(str(item.stats[2]), "+2 to Maximum Damage") + + def test_runeworld_lore(self): + # Lore: Ort Sol + data = bytes.fromhex( + "1008800405c055c637f1073af4558697412981070881506049e87f005516fb134582ff1000a0003500e07cbb001000a0003504e07c9800" + ) + data, item = parse_item(data) + # TODO: requires socket parsing + # self.assertEqual(data, b"") + self.assertTrue(item.is_runeword) + self.assertEqual(item.sockets, 2) + rw = lookup_runeword(item.runeword_id) + self.assertEqual(rw["name"], "Lore") + for stat in item.stats: + print(str(stat)) + self.assertEqual(str(item.stats[4]), "+1 to All Skills") # runeword stat + self.assertEqual(str(item.stats[2]), "+10 to Energy") # runeword stat + # TODO: requires socket parsing + # self.assertEqual(str(item.stats[1]), "Lightning Resist 30%") # sol rune stat