Skip to content

Commit

Permalink
Add mobalytics + small stuff (#263)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisoro authored Jun 2, 2024
1 parent 8ab15d2 commit 626d9cc
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 75 deletions.
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "5.2.0"
__version__ = "5.3.0"
50 changes: 49 additions & 1 deletion src/gui/importer/common.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import datetime
import functools
import re
import time
from collections.abc import Callable
from typing import Literal, TypeVar
Expand All @@ -15,6 +16,7 @@

from src.config.loader import IniConfigLoader
from src.config.models import BrowserType, ProfileModel
from src.item.data.item_type import ItemType
from src.logger import Logger

D = TypeVar("D", bound=WebDriver | WebElement)
Expand All @@ -25,6 +27,50 @@ def extract_digits(text: str) -> int:
return int("".join([char for char in text if char.isdigit()]))


def fix_weapon_type(input_str: str) -> ItemType | None:
input_str = input_str.lower()
if "1h mace" in input_str:
return ItemType.Mace
if "2h mace" in input_str:
return ItemType.Mace2H
if "1h sword" in input_str:
return ItemType.Sword
if "2h sword" in input_str:
return ItemType.Sword2H
if "1h axe" in input_str:
return ItemType.Axe
if "2h axe" in input_str:
return ItemType.Axe2H
if "scythe" in input_str:
return ItemType.Scythe
if "2h scythe" in input_str:
return ItemType.Scythe2H
if "crossbow" in input_str:
return ItemType.Crossbow2H
if "wand" in input_str:
return ItemType.Wand
if "staff" in input_str:
return ItemType.Staff
if "staff" in input_str:
return ItemType.Dagger
return None


def fix_offhand_type(input_str: str, class_str: str) -> ItemType | None:
input_str = input_str.lower()
class_str = class_str.lower()
if "sorc" in class_str:
return ItemType.Focus
if "druid" in class_str:
return ItemType.OffHandTotem
if "necro" in class_str:
if "focus" in input_str or ("offhand" in input_str and "cooldown reduction" in input_str):
return ItemType.Focus
if "shield" in input_str:
return ItemType.Shield
return None


def format_number_as_short_string(n: int) -> str:
result = n / 1_000_000
return f"{int(result)}M" if result.is_integer() else f"{result:.2f}M"
Expand Down Expand Up @@ -85,7 +131,9 @@ def wrapper(*args, **kwargs):


def save_as_profile(file_name: str, profile: ProfileModel, url: str):
file_name = file_name.replace("/", "_").replace(" ", "_").replace("'", "").replace("-", "_")
file_name = file_name.replace("'", "")
file_name = re.sub(r"[ /-]", "_", file_name)
file_name = re.sub(r"_+", "_", file_name)
save_path = IniConfigLoader().user_dir / f"profiles/{file_name}.yaml"
with open(save_path, "w", encoding="utf-8") as file:
file.write(f"# {url}\n")
Expand Down
72 changes: 15 additions & 57 deletions src/gui/importer/d4builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from src.config.models import AffixFilterCountModel, AffixFilterModel, ItemFilterModel, ProfileModel
from src.dataloader import Dataloader
from src.gui.importer.common import (
fix_offhand_type,
fix_weapon_type,
match_to_enum,
retry_importer,
save_as_profile,
Expand Down Expand Up @@ -53,26 +55,23 @@ def import_d4builds(driver: ChromiumDriver = None, url: str = None):
time.sleep(5) # super hacky but I didn't find anything else. The page is not fully loaded when the above wait is done
data = lxml.html.fromstring(driver.page_source)
class_name = data.xpath(CLASS_XPATH)[0].tail.lower()
items = data.xpath(BUILD_OVERVIEW_XPATH)
if not items:
if not (items := data.xpath(BUILD_OVERVIEW_XPATH)):
Logger.error(msg := "No items found")
raise D4BuildsException(msg)
non_unique_slots = _get_non_unique_slots(data=data)
finished_filters = []
for item in items[0]:
item_filter = ItemFilterModel()
slot = item.xpath(ITEM_SLOT_XPATH)[1].tail
if not slot:
if not (slot := item.xpath(ITEM_SLOT_XPATH)[1].tail):
Logger.error("No item_type found")
continue
if slot not in non_unique_slots:
Logger.warning(f"Uniques or empty are not supported. Skipping {slot=}")
Logger.warning(f"Uniques or empty are not supported. Skipping: {slot}")
continue
item_type = None
stats = item.xpath(ITEM_STATS_XPATH)
if not stats:
if not (stats := item.xpath(ITEM_STATS_XPATH)):
Logger.error(f"No stats found for {slot=}")
continue
item_type = None
affixes = []
inherents = []
for stat in stats:
Expand All @@ -81,17 +80,19 @@ def import_d4builds(driver: ChromiumDriver = None, url: str = None):
if "filled" not in stat.xpath("../..")[0].attrib["class"]:
continue
affix_name = stat.xpath("./span")[0].text
if "Weapon" in slot and (x := _fix_weapon_type(affix_name)) is not None:
if "weapon" in slot.lower() and (x := fix_weapon_type(input_str=affix_name)) is not None:
item_type = x
continue
if "Offhand" in slot and (x := _fix_offhand_type(input_str=affix_name, class_str=class_name)) is not None:
if "offhand" in slot.lower() and (x := fix_offhand_type(input_str=affix_name, class_str=class_name)) is not None:
item_type = x
continue
affix_obj = Affix(name=closest_match(clean_str(_corrections(input_str=affix_name)).strip().lower(), Dataloader().affix_dict))
if affix_obj.name is None:
Logger.error(f"Couldn't match {affix_name=}")
continue
if (("Ring" in slot or "Amulet" in slot) and "%" in affix_name) or "Boots" in slot and "Max Evade Charges" in affix_name:
if ("ring" in slot.lower() and any(substring in affix_name.lower() for substring in ["resistance"])) or (
"boots" in slot.lower() and any(substring in affix_name.lower() for substring in ["max evade charges", "attacks reduce"])
):
inherents.append(affix_obj)
else:
affixes.append(affix_obj)
Expand Down Expand Up @@ -131,55 +132,12 @@ def _corrections(input_str: str) -> str:
return input_str


def _fix_offhand_type(input_str: str, class_str: str) -> ItemType | None:
input_str = input_str.lower()
class_str = class_str.lower()
if "offhand" not in input_str:
return None
if "sorc" in class_str:
return ItemType.Focus
if "druid" in class_str:
return ItemType.OffHandTotem
if "necro" in class_str:
if "cooldown reduction" in input_str:
return ItemType.Focus
return ItemType.Shield
return None


def _fix_weapon_type(input_str: str) -> ItemType | None:
input_str = input_str.lower()
if "1h mace" in input_str:
return ItemType.Mace
if "2h mace" in input_str:
return ItemType.Mace2H
if "1h sword" in input_str:
return ItemType.Sword
if "2h sword" in input_str:
return ItemType.Sword2H
if "1h axe" in input_str:
return ItemType.Axe
if "2h axe" in input_str:
return ItemType.Axe2H
if "scythe" in input_str:
return ItemType.Scythe
if "2h scythe" in input_str:
return ItemType.Scythe2H
if "crossbow" in input_str:
return ItemType.Crossbow2H
if "wand" in input_str:
return ItemType.Wand
return None


def _get_non_unique_slots(data: lxml.html.HtmlElement) -> list[str]:
result = []
paperdoll = data.xpath(PAPERDOLL_XPATH)
if not paperdoll:
if not (paperdoll := data.xpath(PAPERDOLL_XPATH)):
Logger.error(msg := "No paperdoll found")
raise D4BuildsException(msg)
items = paperdoll[0].xpath(PAPERDOLL_ITEM_XPATH)
if not items:
if not (items := paperdoll[0].xpath(PAPERDOLL_ITEM_XPATH)):
Logger.error(msg := "No items found")
raise D4BuildsException(msg)
for item in items:
Expand All @@ -193,7 +151,7 @@ def _get_non_unique_slots(data: lxml.html.HtmlElement) -> list[str]:
Logger.init("debug")
os.chdir(pathlib.Path(__file__).parent.parent.parent.parent)
URLS = [
"https://d4builds.gg/builds/f8298a54-dc67-41ab-8232-ddfd32bd80fa",
"https://d4builds.gg/builds/dbad6569-2e78-4c43-a831-c563d0a1e1ad/?var=3",
]
for x in URLS:
import_d4builds(url=x)
34 changes: 24 additions & 10 deletions src/gui/importer/maxroll.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
PLANNER_BASE_URL = "https://maxroll.gg/d4/planner/"


class MaxrollException(Exception):
pass


@retry_importer
def import_maxroll(url: str):
url = url.strip().replace("\n", "")
Expand Down Expand Up @@ -50,19 +54,31 @@ def import_maxroll(url: str):
item_filter = ItemFilterModel()
resolved_item = items[str(item_id)]
if resolved_item["id"] in mapping_data["items"] and mapping_data["items"][resolved_item["id"]]["magicType"] == 2:
Logger.warning(f"Uniques are not supported. Skipping '{mapping_data["items"][resolved_item["id"]]["name"]}'")
Logger.warning(f"Uniques are not supported. Skipping: {mapping_data["items"][resolved_item["id"]]["name"]}")
continue
if (item_type := _find_item_type(mapping_data=mapping_data["items"], value=resolved_item["id"])) is None:
Logger.error("Couldn't find item type")
return
item_filter.itemType = [item_type]
item_filter.affixPool = [
AffixFilterCountModel(
count=[AffixFilterModel(name=x.name) for x in _find_item_affixes(mapping_data, resolved_item)],
count=[
AffixFilterModel(name=x.name)
for x in _find_item_affixes(mapping_data=mapping_data, item_affixes=resolved_item["explicits"])
],
minCount=2,
minGreaterAffixCount=0,
)
]
if "implicits" in resolved_item:
item_filter.inherentPool = [
AffixFilterCountModel(
count=[
AffixFilterModel(name=x.name)
for x in _find_item_affixes(mapping_data=mapping_data, item_affixes=resolved_item["implicits"])
]
)
]
filter_name = item_filter.itemType[0].name
i = 2
while any(filter_name == next(iter(x)) for x in finished_filters):
Expand All @@ -86,17 +102,17 @@ def _corrections(input_str: str) -> str:
return input_str


def _find_item_affixes(mapping_data: dict, item: dict) -> list[Affix]:
def _find_item_affixes(mapping_data: dict, item_affixes: dict) -> list[Affix]:
res = []
for affix_id in item["explicits"]:
for affix_id in item_affixes:
for affix in mapping_data["affixes"].values():
if affix["id"] != affix_id["nid"]:
continue
attr_desc = ""
if "formula" in affix["attributes"][0] and affix["attributes"][0]["formula"] in [
"AffixSingleResist",
"AffixFlatResourceUpto4",
"AffixResourceOnKill",
"AffixSingleResist",
]:
if affix["attributes"][0]["formula"] in ["AffixSingleResist"]:
attr_desc = mapping_data["uiStrings"]["damageType"][str(affix["attributes"][0]["param"])] + " Resistance"
Expand All @@ -118,6 +134,8 @@ def _find_item_affixes(mapping_data: dict, item: dict) -> list[Affix]:
affix_obj = Affix(name=closest_match(clean_str(clean_desc).strip().lower(), Dataloader().affix_dict))
if affix_obj.name is not None:
res.append(affix_obj)
elif "formula" in affix["attributes"][0] and affix["attributes"][0]["formula"] in ["InherentAffixAnyResist_Ring"]:
Logger.info("Skipping InherentAffixAnyResist_Ring")
else:
Logger.error(f"Couldn't match {affix_id=}")
break
Expand Down Expand Up @@ -169,15 +187,11 @@ def _extract_planner_url_and_id_from_guide(url: str) -> tuple[str, int]:
return PLANNER_API_BASE_URL + planner_id, data_id


class MaxrollException(Exception):
pass


if __name__ == "__main__":
Logger.init("debug")
os.chdir(pathlib.Path(__file__).parent.parent.parent.parent)
URLS = [
"https://maxroll.gg/d4/build-guides/double-swing-barbarian-leveling-guide",
"https://maxroll.gg/d4/planner/dqih026y#3",
]
for x in URLS:
import_maxroll(url=x)
Loading

0 comments on commit 626d9cc

Please sign in to comment.