From c02335ab20b8cb6b7c6252d5e50739c21186b4b6 Mon Sep 17 00:00:00 2001 From: chrisoro Date: Tue, 9 Apr 2024 22:15:47 +0200 Subject: [PATCH 1/3] update --- README.md | 12 +-- src/cam.py | 12 +-- src/config/loader.py | 14 +-- src/config/models.py | 138 ++++++++++++++++++++++++----- src/config/ui.py | 42 ++++----- src/dataloader.py | 73 ++++++++-------- src/item/filter.py | 145 ++++++++++++------------------- src/main.py | 8 +- test/config/data/sigils.py | 32 +++++++ test/config/data/uniques.py | 33 +++++++ test/config/models_test.py | 42 +++++++++ test/custom_fixtures.py | 14 +++ test/item/filter/data/filters.py | 60 +++++++------ test/item/filter/filter_test.py | 4 +- 14 files changed, 404 insertions(+), 225 deletions(-) create mode 100644 test/config/data/sigils.py create mode 100644 test/config/data/uniques.py create mode 100644 test/config/models_test.py create mode 100644 test/custom_fixtures.py diff --git a/README.md b/README.md index 42eb3205..e0ddd918 100644 --- a/README.md +++ b/README.md @@ -87,9 +87,9 @@ Affixes have the same structure of `[AFFIX_KEY, THRESHOLD, CONDITION]` as descri ```yaml Affixes: - # Search for armor and pants that have at least 3 affixes of the affixPool + # Search for chest armor and pants that have at least 3 affixes of the affixPool - NiceArmor: - itemType: [armor, pants] + itemType: [chest armor, pants] minPower: 725 affixPool: - [damage_reduction_from_close_enemies, 10] @@ -149,14 +149,14 @@ Uniques: - itemType: pants ``` ```yaml -# Take all unique armor and pants +# Take all unique chest armors and pants Uniques: - - itemType: [armor, pants] + - itemType: [chest armor, pants] ``` ```yaml -# Take all unique armor and pants with min item power > 900 +# Take all unique chest armors and pants with min item power > 900 Uniques: - - itemType: [armor, pants] + - itemType: [chest armor, pants] minPower: 900 ``` ```yaml diff --git a/src/cam.py b/src/cam.py index 5a298e1b..0a32e9bf 100644 --- a/src/cam.py +++ b/src/cam.py @@ -1,15 +1,15 @@ -import mss.windows +import threading +import time -from config.ui import ResManager +import mss.windows mss.windows.CAPTUREBLT = 0 - import numpy as np -import threading from mss import mss -import time -from utils.misc import wait, convert_args_to_numpy + +from config.ui import ResManager from logger import Logger +from utils.misc import wait, convert_args_to_numpy cached_img_lock = threading.Lock() diff --git a/src/config/loader.py b/src/config/loader.py index 6099eee3..92e46b80 100644 --- a/src/config/loader.py +++ b/src/config/loader.py @@ -5,7 +5,7 @@ from pathlib import Path from config.helper import singleton -from config.models import Char, General, AdvancedOptions +from config.models import CharModel, GeneralModel, AdvancedOptionsModel from logger import Logger CONFIG_IN_USER_DIR = ".d4lf" @@ -41,7 +41,7 @@ def _load_params(self): if (p := (Path(USER_DIR) / CONFIG_IN_USER_DIR / PARAMS_INI)).exists() and p.stat().st_size: self._parsers["custom"].read(p) - self._advanced_options = AdvancedOptions( + self._advanced_options = AdvancedOptionsModel( run_scripts=self._select_val("advanced_options", "run_scripts"), run_filter=self._select_val("advanced_options", "run_filter"), exit_key=self._select_val("advanced_options", "exit_key"), @@ -49,8 +49,8 @@ def _load_params(self): scripts=self._select_val("advanced_options", "scripts").split(","), process_name=self._select_val("advanced_options", "process_name"), ) - self._char = Char(inventory=self._select_val("char", "inventory")) - self._general = General( + self._char = CharModel(inventory=self._select_val("char", "inventory")) + self._general = GeneralModel( profiles=self._select_val("general", "profiles").split(","), run_vision_mode_on_startup=self._select_val("general", "run_vision_mode_on_startup"), check_chest_tabs=self._select_val("general", "check_chest_tabs"), @@ -59,19 +59,19 @@ def _load_params(self): ) @property - def advanced_options(self) -> AdvancedOptions: + def advanced_options(self) -> AdvancedOptionsModel: if not self._loaded: self.load() return self._advanced_options @property - def char(self) -> Char: + def char(self) -> CharModel: if not self._loaded: self.load() return self._char @property - def general(self) -> General: + def general(self) -> GeneralModel: if not self._loaded: self.load() return self._general diff --git a/src/config/models.py b/src/config/models.py index e7171d3e..ea283fd0 100644 --- a/src/config/models.py +++ b/src/config/models.py @@ -1,6 +1,8 @@ """New config loading and verification using pydantic. For now, both will exist in parallel hence _new.""" +import enum from pathlib import Path +from typing import List import numpy from pydantic import BaseModel, ConfigDict, field_validator, model_validator @@ -8,13 +10,52 @@ from pydantic_numpy.model import NumpyModel from config.helper import key_must_exist +from item.data.item_type import ItemType -class _IniBase(BaseModel): +class ComparisonType(enum.StrEnum): + larger = enum.auto() + smaller = enum.auto() + + +class _IniBaseModel(BaseModel): model_config = ConfigDict(frozen=True, str_strip_whitespace=True, str_to_lower=True) -class AdvancedOptions(_IniBase): +class AffixAspectFilterModel(BaseModel): + name: str + value: float | None = None + comparison: ComparisonType = ComparisonType.larger + + @field_validator("name") + def name_must_exist(cls, name: str) -> str: + import dataloader # This on module level would be a circular import, so we do it lazy for now + + if name not in dataloader.Dataloader().affix_dict.keys() and name not in dataloader.Dataloader().aspect_unique_dict.keys(): + raise ValueError(f"affix {name} does not exist") + return name + + @model_validator(mode="before") + def parse_data(cls, data: str | list[str] | list[str | float] | dict[str, str | float]) -> dict[str, str | float]: + if isinstance(data, dict): + return data + if isinstance(data, str): + return {"name": data} + if isinstance(data, list): + if not data or len(data) > 3: + raise ValueError("list, cannot be empty or larger than 3 items") + result = {} + if len(data) >= 1: + result["name"] = data[0] + if len(data) >= 2: + result["value"] = data[1] + if len(data) == 3: + result["comparison"] = data[2] + return result + raise ValueError("must be str or list") + + +class AdvancedOptionsModel(_IniBaseModel): exit_key: str log_lvl: str = "info" process_name: str = "Diablo IV.exe" @@ -23,7 +64,7 @@ class AdvancedOptions(_IniBase): scripts: list[str] @model_validator(mode="after") - def key_must_be_unique(self) -> "AdvancedOptions": + def key_must_be_unique(self) -> "AdvancedOptionsModel": keys = [self.exit_key, self.run_filter, self.run_scripts] if len(set(keys)) != len(keys): raise ValueError(f"hotkeys must be unique") @@ -40,7 +81,7 @@ def log_lvl_must_exist(cls, k: str) -> str: return k -class Char(_IniBase): +class CharModel(_IniBaseModel): inventory: str @field_validator("inventory") @@ -48,19 +89,19 @@ def key_must_exist(cls, k: str) -> str: return key_must_exist(k) -class Colors(_IniBase): - aspect_number: "HSVRange" - cold_imbued: "HSVRange" - legendary_orange: "HSVRange" - material_color: "HSVRange" - poison_imbued: "HSVRange" - shadow_imbued: "HSVRange" - skill_cd: "HSVRange" - unique_gold: "HSVRange" - unusable_red: "HSVRange" +class ColorsModel(_IniBaseModel): + aspect_number: "HSVRangeModel" + cold_imbued: "HSVRangeModel" + legendary_orange: "HSVRangeModel" + material_color: "HSVRangeModel" + poison_imbued: "HSVRangeModel" + shadow_imbued: "HSVRangeModel" + skill_cd: "HSVRangeModel" + unique_gold: "HSVRangeModel" + unusable_red: "HSVRangeModel" -class General(_IniBase): +class GeneralModel(_IniBaseModel): check_chest_tabs: int hidden_transparency: float language: str = "enUS" @@ -87,7 +128,7 @@ def path_must_exist(cls, v: Path | None) -> Path | None: return v -class HSVRange(_IniBase): +class HSVRangeModel(_IniBaseModel): h_s_v_min: np_array_pydantic_annotated_typing(dimensions=1) h_s_v_max: np_array_pydantic_annotated_typing(dimensions=1) @@ -101,7 +142,7 @@ def __getitem__(self, index): raise IndexError("Index out of range") @model_validator(mode="after") - def check_interval_sanity(self) -> "HSVRange": + def check_interval_sanity(self) -> "HSVRangeModel": if self.h_s_v_min[0] > self.h_s_v_max[0]: raise ValueError(f"invalid hue range [{self.h_s_v_min[0]}, {self.h_s_v_max[0]}]") if self.h_s_v_min[1] > self.h_s_v_max[1]: @@ -121,7 +162,64 @@ def values_in_range(cls, v: numpy.ndarray) -> numpy.ndarray: return v -class UiOffsets(_IniBase): +class SigilModel(BaseModel): + minTier: int = 0 + maxTier: int = 100 + blacklist: list[str] = [] + whitelist: list[str] = [] + + @model_validator(mode="after") + def blacklist_whitelist_must_be_unique(self) -> "SigilModel": + errors = [item for item in self.blacklist if item in self.whitelist] + if errors: + raise ValueError(f"blacklist and whitelist must not overlap: {errors}") + return self + + @field_validator("maxTier") + def max_tier_in_range(cls, v: int) -> int: + if not 0 <= v <= 100: + raise ValueError("must be in [0, 100]") + return v + + @field_validator("minTier") + def min_tier_in_range(cls, v: int) -> int: + if not 0 <= v <= 100: + raise ValueError("must be in [0, 100]") + return v + + @field_validator("blacklist", "whitelist") + def name_must_exist(cls, names: list[str]) -> list[str]: + import dataloader # This on module level would be a circular import, so we do it lazy for now + + errors = [] + for name in names: + if name not in dataloader.Dataloader().affix_sigil_dict.keys(): + errors.append(name) + if errors: + raise ValueError(f"The following affixes/dungeons do not exist: {errors}") + return names + + +class UniqueModel(BaseModel): + affix: list[AffixAspectFilterModel] = [] + aspect: AffixAspectFilterModel = None + itemType: list[ItemType] = [] + minPower: int = 0 + + @field_validator("itemType", mode="before") + def parse_item_type(cls, data: str | list[str]) -> list[str]: + if isinstance(data, str): + return [data] + return data + + +class ProfileModel(BaseModel): + name: str + Sigils: SigilModel | None = None + Uniques: List[UniqueModel] = [] + + +class UiOffsetsModel(_IniBaseModel): find_bullet_points_width: int find_seperator_short_offset_top: int item_descr_line_height: int @@ -131,12 +229,12 @@ class UiOffsets(_IniBase): vendor_center_item_x: int -class UiPos(_IniBase): +class UiPosModel(_IniBaseModel): possible_centers: list[tuple[int, int]] window_dimensions: tuple[int, int] -class UiRoi(NumpyModel): +class UiRoiModel(NumpyModel): core_skill: np_array_pydantic_annotated_typing(dimensions=1) health_slice: np_array_pydantic_annotated_typing(dimensions=1) hud_detection: np_array_pydantic_annotated_typing(dimensions=1) diff --git a/src/config/ui.py b/src/config/ui.py index ce287274..26e4ff4e 100644 --- a/src/config/ui.py +++ b/src/config/ui.py @@ -3,13 +3,13 @@ import numpy as np from config.helper import singleton -from config.models import UiRoi, UiPos, UiOffsets, Colors, HSVRange +from config.models import UiRoiModel, UiPosModel, UiOffsetsModel, ColorsModel, HSVRangeModel LOGGER = logging.getLogger("d4lf") _FHD = ( (1920, 1080), - UiOffsets( + UiOffsetsModel( find_bullet_points_width=39, find_seperator_short_offset_top=250, item_descr_line_height=25, @@ -18,7 +18,7 @@ item_descr_width=387, vendor_center_item_x=616, ), - UiPos( + UiPosModel( possible_centers=[ (1497, 122), (1497, 216), @@ -36,7 +36,7 @@ ], window_dimensions=(1920, 1080), ), - UiRoi( + UiRoiModel( core_skill=np.array([1094, 981, 47, 47]), health_slice=np.array([587, 925, 43, 130]), hud_detection=np.array([702, 978, 59, 53]), @@ -97,8 +97,8 @@ def _transform_tuples(self, value: tuple[int, int]) -> tuple[int, int]: values = self._transform_array(value=np.array(value, dtype=int)) return int(values[0]), int(values[1]) - def from_fhd(self) -> tuple[UiOffsets, UiPos, UiRoi]: - offsets = UiOffsets( + def from_fhd(self) -> tuple[UiOffsetsModel, UiPosModel, UiRoiModel]: + offsets = UiOffsetsModel( find_bullet_points_width=self._transform(value=_FHD[1].find_bullet_points_width), find_seperator_short_offset_top=self._transform(value=_FHD[1].find_seperator_short_offset_top), item_descr_line_height=self._transform(value=_FHD[1].item_descr_line_height), @@ -107,11 +107,11 @@ def from_fhd(self) -> tuple[UiOffsets, UiPos, UiRoi]: item_descr_width=self._transform(value=_FHD[1].item_descr_width), vendor_center_item_x=self._transform(value=_FHD[1].vendor_center_item_x), ) - pos = UiPos( + pos = UiPosModel( possible_centers=self._transform_list_of_tuples(value=_FHD[2].possible_centers), window_dimensions=self._transform_tuples(value=_FHD[2].window_dimensions), ) - roi = UiRoi( + roi = UiRoiModel( core_skill=self._transform_array(value=_FHD[3].core_skill), health_slice=self._transform_array(value=_FHD[3].health_slice), hud_detection=self._transform_array(value=_FHD[3].hud_detection), @@ -141,15 +141,15 @@ def __init__(self): self._roi = _FHD[3] @property - def offsets(self) -> UiOffsets: + def offsets(self) -> UiOffsetsModel: return self._offsets @property - def pos(self) -> UiPos: + def pos(self) -> UiPosModel: return self._pos @property - def roi(self) -> UiRoi: + def roi(self) -> UiRoiModel: return self._roi def set_resolution(self, res: str): @@ -160,14 +160,14 @@ def set_resolution(self, res: str): self._offsets, self._pos, self._roi = _ResTransformer(resolution=res).from_fhd() -COLORS = Colors( - aspect_number=HSVRange(h_s_v_min=np.array([90, 60, 200]), h_s_v_max=np.array([150, 100, 255])), - cold_imbued=HSVRange(h_s_v_min=np.array([88, 0, 0]), h_s_v_max=np.array([112, 255, 255])), - legendary_orange=HSVRange(h_s_v_min=np.array([4, 190, 190]), h_s_v_max=np.array([26, 255, 255])), - material_color=HSVRange(h_s_v_min=np.array([86, 110, 190]), h_s_v_max=np.array([114, 220, 255])), - poison_imbued=HSVRange(h_s_v_min=np.array([55, 0, 0]), h_s_v_max=np.array([65, 255, 255])), - shadow_imbued=HSVRange(h_s_v_min=np.array([120, 0, 0]), h_s_v_max=np.array([140, 255, 255])), - skill_cd=HSVRange(h_s_v_min=np.array([5, 61, 38]), h_s_v_max=np.array([16, 191, 90])), - unique_gold=HSVRange(h_s_v_min=np.array([4, 45, 125]), h_s_v_max=np.array([26, 155, 250])), - unusable_red=HSVRange(h_s_v_min=np.array([0, 210, 110]), h_s_v_max=np.array([10, 255, 210])), +COLORS = ColorsModel( + aspect_number=HSVRangeModel(h_s_v_min=np.array([90, 60, 200]), h_s_v_max=np.array([150, 100, 255])), + cold_imbued=HSVRangeModel(h_s_v_min=np.array([88, 0, 0]), h_s_v_max=np.array([112, 255, 255])), + legendary_orange=HSVRangeModel(h_s_v_min=np.array([4, 190, 190]), h_s_v_max=np.array([26, 255, 255])), + material_color=HSVRangeModel(h_s_v_min=np.array([86, 110, 190]), h_s_v_max=np.array([114, 220, 255])), + poison_imbued=HSVRangeModel(h_s_v_min=np.array([55, 0, 0]), h_s_v_max=np.array([65, 255, 255])), + shadow_imbued=HSVRangeModel(h_s_v_min=np.array([120, 0, 0]), h_s_v_max=np.array([140, 255, 255])), + skill_cd=HSVRangeModel(h_s_v_min=np.array([5, 61, 38]), h_s_v_max=np.array([16, 191, 90])), + unique_gold=HSVRangeModel(h_s_v_min=np.array([4, 45, 125]), h_s_v_max=np.array([26, 155, 250])), + unusable_red=HSVRangeModel(h_s_v_min=np.array([0, 210, 110]), h_s_v_max=np.array([10, 255, 210])), ) diff --git a/src/dataloader.py b/src/dataloader.py index acfdb046..454d6051 100644 --- a/src/dataloader.py +++ b/src/dataloader.py @@ -1,5 +1,6 @@ import json import threading +from pathlib import Path from config.loader import IniConfigLoader from item.data.item_type import ItemType @@ -31,42 +32,42 @@ def __new__(cls): return cls._instance def load_data(self): - with open(f"assets/lang/{IniConfigLoader().general.language}/affixes.json", "r", encoding="utf-8") as f: + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/affixes.json", "r", encoding="utf-8" + ) as f: self.affix_dict: dict = json.load(f) - with open(f"assets/lang/{IniConfigLoader().general.language}/sigils.json", "r", encoding="utf-8") as f: - affix_sigil_dict_all = json.load(f) - self.affix_sigil_dict = { - **affix_sigil_dict_all["dungeons"], - **affix_sigil_dict_all["minor"], - **affix_sigil_dict_all["major"], - **affix_sigil_dict_all["positive"], - } - - with open(f"assets/lang/{IniConfigLoader().general.language}/corrections.json", "r", encoding="utf-8") as f: - data = json.load(f) - self.error_map = data["error_map"] - self.filter_after_keyword = data["filter_after_keyword"] - self.filter_words = data["filter_words"] - - with open(f"assets/lang/{IniConfigLoader().general.language}/aspects.json", "r", encoding="utf-8") as f: + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/aspects.json", "r", encoding="utf-8" + ) as f: data = json.load(f) for key, d in data.items(): # Note: If you adjust the :68, also adjust it in find_aspect.py self.aspect_dict[key] = d["desc"][:68] self.aspect_num_idx[key] = d["num_idx"] - with open(f"assets/lang/{IniConfigLoader().general.language}/uniques.json", "r", encoding="utf-8") as f: + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/corrections.json", "r", encoding="utf-8" + ) as f: data = json.load(f) - for key, d in data.items(): - # Note: If you adjust the :45, also adjust it in find_aspect.py - self.aspect_unique_dict[key] = d["desc"][:45] - self.aspect_unique_num_idx[key] = d["num_idx"] + self.error_map = data["error_map"] + self.filter_after_keyword = data["filter_after_keyword"] + self.filter_words = data["filter_words"] - with open(f"assets/lang/{IniConfigLoader().general.language}/affixes.json", "r", encoding="utf-8") as f: - self.affix_dict: dict = json.load(f) + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/item_types.json", "r", encoding="utf-8" + ) as f: + data = json.load(f) + for item, value in data.items(): + if item in ItemType.__members__: + enum_member = ItemType[item] + enum_member._value_ = value + else: + Logger.warning(f"{item} type not in item_type.py") - with open(f"assets/lang/{IniConfigLoader().general.language}/sigils.json", "r", encoding="utf-8") as f: + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/sigils.json", "r", encoding="utf-8" + ) as f: affix_sigil_dict_all = json.load(f) self.affix_sigil_dict = { **affix_sigil_dict_all["dungeons"], @@ -75,14 +76,16 @@ def load_data(self): **affix_sigil_dict_all["positive"], } - with open(f"assets/lang/{IniConfigLoader().general.language}/item_types.json", "r", encoding="utf-8") as f: - data = json.load(f) - for item, value in data.items(): - if item in ItemType.__members__: - enum_member = ItemType[item] - enum_member._value_ = value - else: - Logger.warning(f"{item} type not in item_type.py") - - with open(f"assets/lang/{IniConfigLoader().general.language}/tooltips.json", "r", encoding="utf-8") as f: + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/tooltips.json", "r", encoding="utf-8" + ) as f: self.tooltips = json.load(f) + + with open( + Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/uniques.json", "r", encoding="utf-8" + ) as f: + data = json.load(f) + for key, d in data.items(): + # Note: If you adjust the :45, also adjust it in find_aspect.py + self.aspect_unique_dict[key] = d["desc"][:45] + self.aspect_unique_num_idx[key] = d["num_idx"] diff --git a/src/item/filter.py b/src/item/filter.py index df035e2a..f50a305f 100644 --- a/src/item/filter.py +++ b/src/item/filter.py @@ -1,12 +1,13 @@ import os +import sys import time from dataclasses import dataclass, field from pathlib import Path -from typing import Any import yaml from config.loader import IniConfigLoader +from config.models import ProfileModel, UniqueModel, SigilModel, AffixAspectFilterModel, ComparisonType from dataloader import Dataloader from item.data.affix import Affix from item.data.aspect import Aspect @@ -86,7 +87,8 @@ def load_files(self): self.files_loaded = True self.affix_filters = dict() self.aspect_filters = dict() - self.unique_filters = dict() + self.sigil_filters: dict[str, SigilModel] = dict() + self.unique_filters: dict[str, list[UniqueModel]] = dict() profiles: list[str] = IniConfigLoader().general.profiles user_dir = os.path.expanduser("~") @@ -142,26 +144,11 @@ def load_files(self): if config is not None and "Sigils" in config: info_str += "Sigils " - if config["Sigils"] is None: + data = ProfileModel(name=profile_str, **config) + if data.Sigils is None: Logger.error(f"Empty Sigils section in {profile_str}. Remove it") return - self.sigil_filters[profile_str] = config["Sigils"] - # Sanity check on the sigil affixes - if "blacklist" not in self.sigil_filters[profile_str]: - self.sigil_filters[profile_str]["blacklist"] = [] - if "whitelist" not in self.sigil_filters[profile_str]: - self.sigil_filters[profile_str]["whitelist"] = [] - self._check_affix_pool( - self.sigil_filters[profile_str]["blacklist"], Dataloader().affix_sigil_dict, f"{profile_str}.Sigils" - ) - self._check_affix_pool( - self.sigil_filters[profile_str]["whitelist"], Dataloader().affix_sigil_dict, f"{profile_str}.Sigils" - ) - if items_in_both := set(self.sigil_filters[profile_str]["blacklist"]).intersection( - set(self.sigil_filters[profile_str]["whitelist"]) - ): - Logger.error(f"Sigil blacklist and whitelist have overlapping items: {items_in_both}") - return + self.sigil_filters[data.name] = data.Sigils if config is not None and "Aspects" in config: info_str += "Aspects " @@ -179,23 +166,11 @@ def load_files(self): if config is not None and "Uniques" in config: info_str += "Uniques" - if config["Uniques"] is None: + data = ProfileModel(name=profile_str, **config) + if not data.Uniques: Logger.error(f"Empty Uniques section in {profile_str}. Remove it") return - self.unique_filters[profile_str] = config["Uniques"] - # Sanity check for unique aspects - invalid_uniques = [] - for unique in self.unique_filters[profile_str]: - if "aspect" not in unique: - Logger.warning(f"Warning: Unique missing mandatory 'aspect' field in {profile_str} profile") - continue - unique_name = unique["aspect"] if isinstance(unique["aspect"], str) else unique["aspect"][0] - if unique_name not in Dataloader().aspect_unique_dict: - invalid_uniques.append(unique_name) - elif "affixPool" in unique: - self._check_affix_pool(unique["affixPool"], Dataloader().affix_dict, unique_name) - if invalid_uniques: - Logger.warning(f"Warning: Invalid Unique: {', '.join(invalid_uniques)}") + self.unique_filters[data.name] = data.Uniques Logger.info(info_str) @@ -207,51 +182,25 @@ def _did_files_change(self) -> bool: return any(os.path.getmtime(file_path) > self.last_loaded for file_path in self.all_file_pathes) @staticmethod - def _check_item_aspect(filter_data: dict[str, Any], aspect: Aspect) -> bool: - # TODO: really should add configuration schema and validate it once on load so all of these checks in code are not necessary - if "aspect" not in filter_data: - return True - if isinstance(filter_data["aspect"], str): - filter_data["aspect"] = [filter_data["aspect"]] - # check type - if aspect.type != filter_data["aspect"][0]: + def _check_item_aspect(expected_aspect: AffixAspectFilterModel, item_aspect: Aspect) -> bool: + if expected_aspect.name != item_aspect.type: return False - # check value - if len(filter_data["aspect"]) > 1: - if aspect.value is None: + if expected_aspect.value is not None: + if item_aspect.value is None: return False - threshold = filter_data["aspect"][1] - condition = filter_data["aspect"][2] if len(filter_data["aspect"]) > 2 else "larger" - if not ( - threshold is None - or (isinstance(condition, str) and condition == "larger" and aspect.value >= threshold) - or (isinstance(condition, str) and condition == "smaller" and aspect.value <= threshold) + if not (expected_aspect.comparison == ComparisonType.larger and item_aspect.value >= expected_aspect.value) or ( + expected_aspect.comparison == ComparisonType.smaller and item_aspect.value <= expected_aspect.value ): return False return True @staticmethod - def _check_item_power(filter_data: dict[str, Any], power: int, min_key: str = "minPower", max_key: str = "maxPower") -> bool: - # TODO: really should add configuration schema and validate it once on load so all of these checks in code are not necessary - min_power = filter_data[min_key] if min_key in filter_data and filter_data[min_key] is not None else 1 - if not isinstance(min_power, int): - Logger.warning(f"{min_key} ({min_power}) is not an integer!") - return False - max_power = filter_data[max_key] if max_key in filter_data and filter_data[max_key] is not None else 9999 - if not isinstance(max_power, int): - Logger.warning(f"{max_key} ({max_power}) is not an integer!") - return False - return min_power <= power <= max_power + def _check_item_power(min_power: int, item_power: int, max_power: int = sys.maxsize) -> bool: + return min_power <= item_power <= max_power @staticmethod - def _check_item_type(filter_data: dict[str, Any], item_type: ItemType) -> bool: - # TODO: really should add configuration schema and validate it once on load so all of these checks in code are not necessary - if "itemType" not in filter_data: - return True - filter_item_type_list = [ - ItemType(val) for val in ([filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"]) - ] - return item_type in filter_item_type_list + def _check_item_type(expected_item_types: list[ItemType], item_type: ItemType) -> bool: + return item_type in expected_item_types def _match_affixes(self, filter_affix_pool: list, item_affix_pool: list[Affix]) -> list: item_affix_pool = item_affix_pool[:] @@ -275,9 +224,9 @@ def _match_affixes(self, filter_affix_pool: list, item_affix_pool: list[Affix]) item_affix_value = next((a.value for a in item_affix_pool if a.type == name), None) if item_affix_value is not None: if ( - threshold is None - or (isinstance(condition, str) and condition == "larger" and item_affix_value >= threshold) - or (isinstance(condition, str) and condition == "smaller" and item_affix_value <= threshold) + threshold is None + or (isinstance(condition, str) and condition == "larger" and item_affix_value >= threshold) + or (isinstance(condition, str) and condition == "smaller" and item_affix_value <= threshold) ): item_affix_pool = [a for a in item_affix_pool if a.type != name] matched_affixes.append(name) @@ -298,8 +247,19 @@ def _check_non_unique_item(self, item: Item) -> FilterResult: if "minAffixCount" in filter_data and filter_data["minAffixCount"] is not None else 0 ) - power_ok = self._check_item_power(filter_data, item.power) - type_ok = self._check_item_type(filter_data, item.type) + max_power = filter_data["maxPower"] if "maxPower" in filter_data and filter_data["maxPower"] is not None else 9999 + min_power = filter_data["minPower"] if "minPower" in filter_data and filter_data["minPower"] is not None else 1 + power_ok = self._check_item_power(max_power=max_power, min_power=min_power, item_power=item.power) + if "itemType" not in filter_data: + type_ok = True + else: + filter_item_type_list = [ + ItemType(val) + for val in ( + [filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"] + ) + ] + type_ok = self._check_item_type(filter_item_type_list, item.type) if not power_ok or not type_ok: continue matched_affixes = self._match_affixes(filter_data["affixPool"], item.affixes) @@ -325,10 +285,10 @@ def _check_non_unique_item(self, item: Item) -> FilterResult: if item.aspect.type == aspect_name: if ( - threshold is None - or item.aspect.value is None - or (isinstance(condition, str) and condition == "larger" and item.aspect.value >= threshold) - or (isinstance(condition, str) and condition == "smaller" and item.aspect.value <= threshold) + threshold is None + or item.aspect.value is None + or (isinstance(condition, str) and condition == "larger" and item.aspect.value >= threshold) + or (isinstance(condition, str) and condition == "smaller" and item.aspect.value <= threshold) ): Logger.info(f"Matched {profile_str}.Aspects: [{item.aspect.type}, {item.aspect.value}]") res.keep = True @@ -337,17 +297,19 @@ def _check_non_unique_item(self, item: Item) -> FilterResult: def _check_sigil(self, item: Item) -> FilterResult: res = FilterResult(False, []) - if len(self.sigil_filters.items()) == 0: + if ( + len(self.sigil_filters.items()) == 0 + ): # in this intermedia version there are no profiles with Sigils = None since they would have been filtered on load res.keep = True res.matched.append(MatchedFilter("")) for profile_name, profile_filter in self.sigil_filters.items(): # check item power - if not self._check_item_power(profile_filter, item.power, min_key="minTier", max_key="maxTier"): + if not self._check_item_power(max_power=profile_filter.maxTier, min_power=profile_filter.minTier, item_power=item.power): continue - # check affix - if "blacklist" in profile_filter and self._match_affixes(profile_filter["blacklist"], item.affixes + item.inherent): + # check affix TODO + if profile_filter.blacklist and self._match_affixes(profile_filter["blacklist"], item.affixes + item.inherent): continue - if "whitelist" in profile_filter and not self._match_affixes(profile_filter["whitelist"], item.affixes + item.inherent): + if profile_filter.whitelist and not self._match_affixes(profile_filter["whitelist"], item.affixes + item.inherent): continue Logger.info(f"Matched {profile_name}.Sigils") res.keep = True @@ -355,20 +317,23 @@ def _check_sigil(self, item: Item) -> FilterResult: return res def _check_unique_item(self, item: Item) -> FilterResult: - # TODO: really should add configuration schema and validate it once on load so all of these checks in code are not necessary res = FilterResult(False, []) for profile_name, profile_filter in self.unique_filters.items(): for filter_item in profile_filter: # check item type - if not self._check_item_type(filter_item, item.type): + if not self._check_item_type(expected_item_types=filter_item.itemType, item_type=item.type): continue # check item power - if not self._check_item_power(filter_item, item.power): + if not self._check_item_power(min_power=filter_item.minPower, item_power=item.power): continue # check aspect - if item.aspect is None or not self._check_item_aspect(filter_item, item.aspect): + if ( + item.aspect is None + or filter_item.aspect is None + or not self._check_item_aspect(expected_aspect=filter_item.aspect, item_aspect=item.aspect) + ): continue - # check affixes + # check affixes TODO filter_item.setdefault("affixPool", []) matched_affixes = self._match_affixes([] if "affixPool" not in filter_item else filter_item["affixPool"], item.affixes) if len(matched_affixes) != len(filter_item["affixPool"]): diff --git a/src/main.py b/src/main.py index 31a3630d..4ee80e12 100644 --- a/src/main.py +++ b/src/main.py @@ -1,21 +1,18 @@ import os import traceback from pathlib import Path -from PIL import Image # Somehow needed, otherwise the binary has an issue with tesserocr import keyboard from beautifultable import BeautifulTable -from cam import Cam from config.loader import IniConfigLoader from item.filter import Filter from logger import Logger from overlay import Overlay from utils.game_settings import is_fontsize_ok -from utils.misc import wait from utils.ocr.read import load_api from utils.process_handler import safe_exit -from utils.window import start_detecting_window, WindowSpec +from utils.window import WindowSpec from version import __version__ @@ -29,9 +26,6 @@ def main(): Logger.init("info") win_spec = WindowSpec(IniConfigLoader().advanced_options.process_name) - start_detecting_window(win_spec) - while not Cam().is_offset_set(): - wait(0.2) load_api() diff --git a/test/config/data/sigils.py b/test/config/data/sigils.py new file mode 100644 index 00000000..eb32a85d --- /dev/null +++ b/test/config/data/sigils.py @@ -0,0 +1,32 @@ +all_bad_cases = [ + # 1 item + {"Sigil": {"blacklist": "monster_cold_resist"}}, + {"Sigil": {"blacklist": "monster_cold_resist"}}, + {"Sigil": {"blacklist": ["monster123_cold_resist"]}}, + {"Sigil": {"blacklist": ["monster_cold_resist", "test123"]}}, + {"Sigil": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_cold_resist"]}}, + {"Sigil": {"maxTier": 101}}, + {"Sigil": {"minTier": -1}}, + {"Sigil": {"whitelist": ["monster123_cold_resist"]}}, + {"Sigil": {"whitelist": ["monster_cold_resist", "test123"]}}, +] + +all_good_cases = [ + # 1 item + {"Sigil": {"blacklist": ["monster_cold_resist"]}}, + {"Sigil": {"maxTier": 90}}, + {"Sigil": {"minTier": 10}}, + {"Sigil": {"whitelist": ["monster_cold_resist"]}}, + # 2 items + {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90}}, + {"Sigil": {"blacklist": ["monster_cold_resist"], "minTier": 10}}, + {"Sigil": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_fire_resist"]}}, + {"Sigil": {"maxTier": 90, "minTier": 10}}, + {"Sigil": {"maxTier": 90, "whitelist": ["monster_cold_resist"]}}, + {"Sigil": {"minTier": 10, "whitelist": ["monster_cold_resist"]}}, + # 3 items + {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "minTier": 10}}, + {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "whitelist": ["monster_fire_resist"]}}, + {"Sigil": {"blacklist": ["monster_cold_resist"], "minTier": 10, "whitelist": ["monster_fire_resist"]}}, + {"Sigil": {"maxTier": 90, "minTier": 10, "whitelist": ["monster_cold_resist"]}}, +] diff --git a/test/config/data/uniques.py b/test/config/data/uniques.py new file mode 100644 index 00000000..dc1f0818 --- /dev/null +++ b/test/config/data/uniques.py @@ -0,0 +1,33 @@ +all_bad_cases = [ + {"Uniques": [{"affix": "test"}]}, # not a list + {"Uniques": [{"affix": [12]}]}, # list but bad type + {"Uniques": [{"affix": [["damage_reduction_from_close_enemies", "asd"]]}]}, # list but bad type + {"Uniques": [{"affix": [["damage_reduction_from_close_enemies", 12, "bigger"]]}]}, # list but bad type +] + +all_good_cases = { + "name": "good", + "Uniques": [ + # 1 filter criteria + {"affix": ["damage_reduction_from_close_enemies"]}, + {"affix": [["damage_reduction_from_close_enemies", 12, "smaller"], ["damage_reduction_from_distant_enemies", 12, "smaller"]]}, + {"affix": [["damage_reduction_from_close_enemies", 12, "smaller"]]}, + {"affix": [["damage_reduction_from_close_enemies", 12]]}, + {"aspect": "tibaults_will"}, + {"itemType": "pants"}, + {"itemType": ["chest armor", "pants"]}, + {"minPower": 900}, + # 2 filter criterias + {"affix": ["damage_reduction_from_close_enemies"], "aspect": "tibaults_will"}, + {"affix": ["damage_reduction_from_close_enemies"], "itemType": "pants"}, + {"affix": ["damage_reduction_from_close_enemies"], "minPower": 900}, + {"aspect": "tibaults_will", "itemType": "pants"}, + {"aspect": "tibaults_will", "minPower": 900}, + {"itemType": "pants", "minPower": 900}, + # 3 filter criterias + {"affix": ["damage_reduction_from_close_enemies"], "aspect": "tibaults_will", "itemType": "pants"}, + {"affix": ["damage_reduction_from_close_enemies"], "aspect": "tibaults_will", "minPower": 900}, + {"affix": ["damage_reduction_from_close_enemies"], "itemType": "pants", "minPower": 900}, + {"aspect": "tibaults_will", "itemType": "pants", "minPower": 900}, + ], +} diff --git a/test/config/models_test.py b/test/config/models_test.py new file mode 100644 index 00000000..6a349287 --- /dev/null +++ b/test/config/models_test.py @@ -0,0 +1,42 @@ +from typing import Any + +import pytest +from pydantic import ValidationError + +from config.models import ProfileModel +from test.config.data import sigils +from test.config.data import uniques +from test.custom_fixtures import mock_ini_loader + + +class TestSigil: + @pytest.fixture(autouse=True) + def setup(self, mock_ini_loader): + self.mock_ini_loader = mock_ini_loader + + @pytest.mark.parametrize("data", sigils.all_bad_cases) + def test_all_bad_cases(self, data: dict[str, Any]): + with pytest.raises(ValidationError): + data["name"] = "bad" + ProfileModel(**data) + + @pytest.mark.parametrize("data", sigils.all_good_cases) + def test_all_good_cases(self, data: dict[str, Any]): + data["name"] = "good" + assert ProfileModel(**data) + + +class TestUnique: + + @pytest.fixture(autouse=True) + def setup(self, mock_ini_loader): + self.mock_ini_loader = mock_ini_loader + + @pytest.mark.parametrize("data", uniques.all_bad_cases) + def test_all_bad_cases(self, data: dict[str, Any]): + with pytest.raises(ValidationError): + data["name"] = "bad" + ProfileModel(**data) + + def test_all_good_cases(self): + assert ProfileModel(**uniques.all_good_cases) diff --git a/test/custom_fixtures.py b/test/custom_fixtures.py new file mode 100644 index 00000000..0bcf8fcb --- /dev/null +++ b/test/custom_fixtures.py @@ -0,0 +1,14 @@ +import pytest +from pytest_mock import MockerFixture + +from config.loader import IniConfigLoader + + +@pytest.fixture +def mock_ini_loader(mocker: MockerFixture): + mocker.patch.object(IniConfigLoader(), "_loaded", True) + general_mock = mocker.patch.object( + IniConfigLoader(), + "_general", + ) + general_mock.language = "enUS" diff --git a/test/item/filter/data/filters.py b/test/item/filter/data/filters.py index f62dd017..4cd327ac 100644 --- a/test/item/filter/data/filters.py +++ b/test/item/filter/data/filters.py @@ -1,3 +1,6 @@ +from config.models import UniqueModel, AffixAspectFilterModel, ComparisonType, ProfileModel, SigilModel +from item.data.item_type import ItemType + affix = { "test": [ { @@ -77,36 +80,31 @@ ["of_might", 6.0, "smaller"], ] } -sigil = { - "test": { - "blacklist": ["reduce_cooldowns_on_kill", "vault_of_copper"], - "whitelist": ["jalals_vigil"], - "maxTier": 80, - "minTier": 40, - } -} +sigil = ProfileModel( + name="test", + Sigils=SigilModel(blacklist=["reduce_cooldowns_on_kill", "vault_of_copper"], whitelist=["jalals_vigil"], maxTier=80, minTier=40), +) -unique = { - "test": [ - {"itemType": ["scythe", "sword"], "minPower": 900}, - {"itemType": "scythe", "minPower": 900}, - { - "aspect": ["lidless_wall", 20], - "minPower": 900, - "affixPool": [ - ["attack_speed", 8.4], - ["lucky_hit_up_to_a_chance_to_restore_primary_resource", 12], - ["maximum_life", 700], - ["maximum_essence", 9], +unique = ProfileModel( + name="test", + Uniques=[ + UniqueModel(itemType=[ItemType.Scythe, ItemType.Sword], minPower=900), + UniqueModel(itemType=[ItemType.Scythe], minPower=900), + UniqueModel( + affix=[ + AffixAspectFilterModel(name="attack_speed", value=8.4), + AffixAspectFilterModel(name="lucky_hit_up_to_a_chance_to_restore_primary_resource", value=12), + AffixAspectFilterModel(name="maximum_life", value=700), + AffixAspectFilterModel(name="maximum_essence", value=9), ], - }, - { - "aspect": ["soulbrand", 20], - "minPower": 900, - "affixPool": [["attack_speed", 8.4]], - }, - { - "aspect": ["soulbrand", 15, "smaller"], - }, - ] -} + aspect=AffixAspectFilterModel(name="lidless_wall", value=20), + minPower=900, + ), + UniqueModel( + affix=[AffixAspectFilterModel(name="attack_speed", value=8.4)], + aspect=AffixAspectFilterModel(name="soulbrand", value=20), + minPower=900, + ), + UniqueModel(aspect=AffixAspectFilterModel(name="soulbrand", value=15, comparison=ComparisonType.smaller)), + ], +) diff --git a/test/item/filter/filter_test.py b/test/item/filter/filter_test.py index d4254618..3834ee20 100644 --- a/test/item/filter/filter_test.py +++ b/test/item/filter/filter_test.py @@ -37,12 +37,12 @@ def test_aspects(name: str, result: list[str], item: Item, mocker: MockerFixture @pytest.mark.parametrize("name, result, item", natsorted(sigils), ids=[name for name, _, _ in natsorted(sigils)]) def test_sigils(name: str, result: list[str], item: Item, mocker: MockerFixture): test_filter = _create_mocked_filter(mocker) - test_filter.sigil_filters = filters.sigil + test_filter.sigil_filters = {filters.sigil.name: filters.sigil.Sigils} assert natsorted([match.profile.split(".")[0] for match in test_filter.should_keep(item).matched]) == natsorted(result) @pytest.mark.parametrize("name, result, item", natsorted(uniques), ids=[name for name, _, _ in natsorted(uniques)]) def test_uniques(name: str, result: list[str], item: Item, mocker: MockerFixture): test_filter = _create_mocked_filter(mocker) - test_filter.unique_filters = filters.unique + test_filter.unique_filters = {filters.unique.name: filters.unique.Uniques} assert natsorted([match.profile for match in test_filter.should_keep(item).matched]) == natsorted(result) From 7c67b388f474d29fc7d5e7d734dfe1ebbd165b4f Mon Sep 17 00:00:00 2001 From: chrisoro Date: Fri, 19 Apr 2024 17:23:17 +0200 Subject: [PATCH 2/3] update --- README.md | 297 +++++++++++++-------- config/params.ini | 2 +- config/profiles/barb.yaml | 85 ------ config/profiles/druid.yaml | 80 ------ config/profiles/example.yaml | 88 ++++++ config/profiles/general.yaml | 95 ------- config/profiles/necro.yaml | 104 -------- config/profiles/rogue.yaml | 217 --------------- config/profiles/sigils.yaml | 11 - config/profiles/sorc.yaml | 109 -------- config/profiles/uniques.yaml | 89 ------- src/config/models.py | 92 +++++-- src/dataloader.py | 29 +- src/item/data/aspect.py | 2 +- src/item/descr/item_type.py | 35 ++- src/item/descr/read_descr.py | 16 +- src/item/filter.py | 441 +++++++++++++------------------ src/item/models.py | 6 +- src/loot_filter.py | 9 +- src/main.py | 7 +- src/scripts/vision_mode.py | 9 +- src/template_finder.py | 4 +- src/utils/custom_mouse.py | 14 +- src/utils/window.py | 2 +- test/config/data/sigils.py | 46 ++-- test/item/filter/data/affixes.py | 36 +-- test/item/filter/data/aspects.py | 4 +- test/item/filter/data/filters.py | 186 ++++++++----- test/item/filter/data/sigils.py | 4 +- test/item/filter/data/uniques.py | 10 +- test/item/filter/filter_test.py | 10 +- 31 files changed, 764 insertions(+), 1375 deletions(-) delete mode 100644 config/profiles/barb.yaml delete mode 100644 config/profiles/druid.yaml create mode 100644 config/profiles/example.yaml delete mode 100644 config/profiles/general.yaml delete mode 100644 config/profiles/necro.yaml delete mode 100644 config/profiles/rogue.yaml delete mode 100644 config/profiles/sigils.yaml delete mode 100644 config/profiles/sorc.yaml delete mode 100644 config/profiles/uniques.yaml diff --git a/README.md b/README.md index 3f6d9d4b..c3897add 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,144 @@ # ![logo](assets/logo.png) -Filter items and sigils in your inventory based on affixes, aspects and thresholds of their values. For questions, feature request or issue reports join the [discord](https://discord.gg/YyzaPhAN6T) or use github issues. +Filter items and sigils in your inventory based on affixes, aspects and thresholds of their values. For questions, +feature request or issue reports join the [discord](https://discord.gg/YyzaPhAN6T) or use github issues. ![sample](assets/thumbnail.jpg)] ## Features + - Filter items in inventory and stash - Filter by item type and item power - Filter by affix and their values - Filter by aspects and their values -- Filter uniques and their affix and aspect values -- Filter sigils by blacklisting locations and affixes -- Supported resolutions: 1080p, 1440p, 1600p, 2160p +- Filter uniques by their affix and aspect values +- Filter sigils by blacklisting and whitelisting locations and affixes +- Supported resolutions: 1080p, 1440p, 1600p, 2160p - others might work as well, but untested ## How to Setup ### Game Settings + - Font size can be small or medium (better tested on small) in the Gameplay Settings - Game Language must be English ### Run + - Download the latest version (.zip) from the releases: https://github.com/aeon0/d4lf/releases - Execute d4lf.exe and go to your D4 screen - There is a small overlay on the center bottom with buttons: - - max/min: Show or hide the console output - - filter: Auto filter inventory and stash if open (number of stash tabs configurable) - - vision: Turn vision mode (overlay) on/off + - max/min: Show or hide the console output + - filter: Auto filter inventory and stash if open (number of stash tabs configurable) + - vision: Turn vision mode (overlay) on/off - Alternative use the hotkeys. e.g. f11 for filtering - ### Limitations + - The tool does not play well with HDR as it makes everything super bright -- The advanced item comparision feature sometimes causes bad classifications. Better turn it off when using d4lf +- The advanced item comparison feature might cause incorrect classifications ### Configs + The config folder contains: -- __profiles/*.yaml__: These files determine what should be filtered. -- __params.ini__: Different hotkey settings and number of chest stashes that should be looked at. -- __game.ini__: Settings regarding color thresholds and image positions. You dont need to touch this. + +- __profiles/*.yaml__: These files determine what should be filtered +- __params.ini__: Different hotkey settings and number of chest stashes that should be looked at ### params.ini -| [general] | Description | -| ----------------------- | --------------------------------------| -| profiles | A set of profiles seperated by comma. d4lf will look for these yaml files in config/profiles and in C:/Users/WINDOWS_USER/.d4lf/profiles. | -| run_vision_mode_on_startup | If the vision mode should automatically start when starting d4lf. Otherwise has to be started manually with the vision button or the hotkey. | -| check_chest_tabs | Which chest tabs will be checked and fitlered for items in case chest is open when starting the filter. Counting is done left to right. E.g. 1,2,4 will check tab 1, tab 2, tab 4. | -| hidden_transparency | The overlay will go more transparent after not hovering it for a while. This can be any value between [0, 1] with 0 being completely invisible and 1 completely visible. Note the default "visible" transparancy is 0.89 | -| local_prefs_path | In case your prefs file is not found in the Documents there will be a warning about it. You can remove this warning by providing the correct path to your LocalPrefs.txt file | - -| [char] | Description | -| ----------------------- | --------------------------------------| -| inventory | Hotkey for opening inventory | - -| [advanced_options] | Description | -| ----------------------- | --------------------------------------| -| run_scripts | Hotkey to start/stop vision mode | -| run_filter | Hotkey to start/stop filtering items | -| exit_key | Hotkey to exit d4lf.exe | -| log_level | Logging level. Can be any of [debug, info, warning, error] | -| scripts | Running different scripts | -| process_name | Process name of the D4 app. Defaults to "Diablo IV.exe". In case of using some remote play this might need to be adapted | - -## How to filter - -All items are whitelist filters. If a filter for an unique or a certain item type is not included in your .yaml filters, they will be discarded. All sigils are blacklist filted, meaning by default all sigils are good unless they match any of the blacklisted affixes. See more detailed descriptions below. + +| [general] | Description | +|----------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| profiles | A set of profiles seperated by comma. d4lf will look for these yaml files in config/profiles and in C:/Users/WINDOWS_USER/.d4lf/profiles | +| run_vision_mode_on_startup | If the vision mode should automatically start when starting d4lf. Otherwise has to be started manually with the vision button or the hotkey | +| check_chest_tabs | Which chest tabs will be checked and filtered for items in case chest is open when starting the filter. Counting is done left to right. E.g. 1,2,4 will check tab 1, tab 2, tab 4 | +| hidden_transparency | The overlay will become transparent after not hovering it for a while. This can be changed by specifying any value between [0, 1] with 0 being completely invisible and 1 completely visible | +| local_prefs_path | In case your prefs file is not found in the Documents there will be a warning about it. You can remove this warning by providing the correct path to your LocalPrefs.txt file | + +| [char] | Description | +|-----------|-----------------------------------| +| inventory | Your hotkey for opening inventory | + +| [advanced_options] | Description | +|--------------------|--------------------------------------------------------------------------------------------------------------------------| +| run_scripts | Hotkey to start/stop vision mode | +| run_filter | Hotkey to start/stop filtering items | +| exit_key | Hotkey to exit d4lf.exe | +| log_level | Logging level. Can be any of [debug, info, warning, error, critical] | +| scripts | Running different scripts | +| process_name | Process name of the D4 app. Defaults to "Diablo IV.exe". In case of using some remote play this might need to be adapted | + +## How to filter / Profiles + +All profiles define whitelist filters. If no filter included in your profiles matches the item, it will be discarded. + +Your config files will be validated on startup and will prevent the program from starting if the structure or syntax is +incorrect. The error message will provide hints about the specific problem. + +The following sections will explain each type of filter that you can specify in your profiles. How you define them in +your YAML files is up to you; you can put all of these into just one file or have a dedicated file for each type of +filter, or even split the same type of filter over multiple files. Ultimately, all profiles specified in +your `params.ini` will be used to determine if an item should be kept. If one of the profiles wants to keep the item, it +will be kept regardless of the other profiles. ### Aspects -In your profile .yaml files any aspects can be added in the format of `[ASPECT_KEY, THRESHOLD, CONDITION]`. The condition can be any of `[larger, smaller]` and defaults to `larger` if no value is given. Smaller has to be used when the aspect go from high value to a lower value (eg. ā€¨Blood-bathed Aspect) + +Aspects are defined by the top-level key `Aspects`. It contains a list of aspects that you want to filter for. If no +Aspect filter is provided, all legendary items will be kept. You have two choices on how to specify an item: + +- You can use the shorthand and just specify the aspect name +- For more sophisticated filtering, you can use the following syntax: `[ASPECT_NAME, THRESHOLD, CONDITION]`. The + condition can be any of `[larger, smaller]` and defaults to `larger` if no value is given. "Smaller" must be used + when the aspect goes from a high value to a lower value (e.g., `Blood-bathed` Aspect)
Config Examples ```yaml Aspects: - # Filter for a perfect umbral - - [of_the_umbral, 4] - # Filter for any umbral - - of_the_umbral + # Filter for any umbral + - of_the_umbral + # Filter for a perfect umbral + - [ of_the_umbral, 4 ] + # Filter for all but perfect umbral + - [ of_the_umbral, 3.5, smaller ] ``` +
-Aspect keys are lower case and spaces are replaced by underscore. You can find the full list of keys in [assets/lang/enUS/aspect.json](assets/lang/enUS/aspects.json). If Aspects is empty, all legendary items will be kept. +Aspect names are lower case and spaces are replaced by underscore. You can find the full list of names +in [assets/lang/enUS/aspect.json](assets/lang/enUS/aspects.json). ### Affixes -Affixes have the same structure of `[AFFIX_KEY, THRESHOLD, CONDITION]` as described above. Additionally, it can be filtered by `itemType`, `minPower` and `minAffixCount`. See the list of affix keys in [assets/lang/enUS/affixes.json](assets/lang/enUS/affixes.json). For items with inherent affixes `inherentPool` can be specified, then at least one of these has to match the inherent affixes on that item. + +Affixes are defined by the top-level key `Affixes`. It contains a list of filters that you want to apply. Each filter +has a name and can filter for any combination of the following: + +- `itemType`: Either the name of THE type or a list of multiple types. + See [assets/lang/enUS/item_types.json](assets/lang/enUS/item_types.json) +- `minPower`: Minimum item power +- `affixPool`: A list of multiple different rulesets to filter for. Each ruleset must be fulfilled or the item is + discarded + - `count`: Define a list of affixes (same syntax as for [Aspects](#Aspects)) and optionally `minCount` + and `maxCount`. `minCount` specifies the minimum number of affixes that must match the item, `maxCount` the + maximum number. If neither `minCount` nor `maxCount` is provided, all defined affixes must match +- `inherentPool`: The same rules as for `affixPool` apply, but this is evaluated against the inherent affixes of the + item +
Config Examples ```yaml Affixes: - # Search for chest armor and pants that have at least 3 affixes of the affixPool + # Search for chest armor and pants that are at least item level 725 and have at least 3 affixes of the affixPool - NiceArmor: - itemType: [chest armor, pants] + itemType: [ chest armor, pants ] minPower: 725 affixPool: - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction, 5] - - [total_armor, 9] - - [maximum_life, 700] - minAffixCount: 3 + - count: + - [ damage_reduction_from_close_enemies, 10 ] + - [ damage_reduction_from_distant_enemies, 12 ] + - [ damage_reduction, 5 ] + - [ total_armor, 9 ] + - [ maximum_life, 700 ] + minCount: 3 # Search for boots that have at least 2 of the specified affixes and # either max evade charges or reduced evade cooldown as inherent affix @@ -105,37 +146,85 @@ Affixes: itemType: boots minPower: 800 inherentPool: - - maximum_evade_charges - - attacks_reduce_evades_cooldown + - count: + - maximum_evade_charges + - attacks_reduce_evades_cooldown + minCount: 1 affixPool: - - [movement_speed, 16] - - [cold_resistance] - - [lightning_resistance] - minAffixCount: 2 - - # Search with "any_of" affixPool. Any of these will be ok, but they will only contribute - # once to the overall matched affix count. Any affix on the item can only match once! - # Example: We want to have boots with movement speed and 2 resistances from a pool of shadow, cold, lightning res + - count: + - [ movement_speed, 16 ] + - [ cold_resistance ] + - [ lightning_resistance ] + minCount: 2 + + # Search for boots with movement speed and 2 resistances from a pool of shadow, cold, lightning res - ResBoots: itemType: boots minPower: 800 affixPool: - - [movement_speed, 16] - - any_of: - - [shadow_resistance] - - [cold_resistance] - - [lightning_resistance] - - any_of: - - [shadow_resistance] - - [cold_resistance] - - [lightning_resistance] - minAffixCount: 3 + - count: + - [ movement_speed, 16 ] + - count: + - [ shadow_resistance ] + - [ cold_resistance ] + - [ lightning_resistance ] + minCount: 2 + +``` + +
+Affix names are lower case and spaces are replaced by underscore. You can find the full list of names +in [assets/lang/enUS/affixes.json](assets/lang/enUS/affixes.json). + +### Sigils + +Sigils are defined by the top-level key `Sigils`. It contains a list of affix or location names that you want to filter +for. If no Sigil filter is provided, all Sigils will be kept. + +
Config Examples + +```yaml +Sigils: + minTier: 40 + maxTier: 100 + blacklist: + # locations + - endless_gates + - vault_of_the_forsaken + + # affixes + - armor_breakers + - resistance_breakers +``` + +If you want to filter for a specific affix or location, you can also use the `whitelist` key. Even if `whitelist` is +present, `blacklist` will be used to discard sigils that match any of the blacklisted affixes or locations. + +```yaml +# Only keep sigils for vault_of_the_forsaken without any of the affixes armor_breakers and resistance_breakers +Sigils: + minTier: 40 + maxTier: 100 + blacklist: + - armor_breakers + - resistance_breakers + whitelist: + - vault_of_the_forsaken ``` +
+Sigil affixes and location names are lower case and spaces are replaced by underscore. You can find the full list of +names in [assets/lang/enUS/sigils.json](assets/lang/enUS/sigils.json). + ### Uniques -Uniques are identified by their `item power`, [item type](assets/lang/enUS/item_types.json) and [aspect](assets/lang/enUS/uniques.json). They also have [affixes](assets/lang/enUS/affixes.json), but since uniques have these fixed you only need to specify the ones you want to threshold. + +Uniques are defined by the top-level key `Uniques`. It contains a list of parameters that you want to filter for. If no +Unique filter is provided, all unique items will be kept. +Uniques can be filtered similar to [Affixes](#Affixes) and [Aspects](#Aspects), but due to their nature of fixed +effects, you only have to specify the thresholds that you want to apply. +
Config Examples ```yaml @@ -143,77 +232,63 @@ Uniques are identified by their `item power`, [item type](assets/lang/enUS/item_ Uniques: - minPower: 900 ``` + ```yaml # Take all unique pants Uniques: - itemType: pants ``` + ```yaml # Take all unique chest armors and pants Uniques: - - itemType: [chest armor, pants] + - itemType: [ chest armor, pants ] ``` + ```yaml # Take all unique chest armors and pants with min item power > 900 Uniques: - - itemType: [chest armor, pants] + - itemType: [ chest armor, pants ] minPower: 900 ``` + ```yaml # Take all Tibault's Will pants Uniques: - - aspect: [tibaults_will] + - aspect: [ tibaults_will ] ``` + ```yaml # Take all Tibault's Will pants that have item power > 900 and dmg reduction from close > 12 as well as aspect value > 25 Uniques: - - aspect: [tibaults_will] + - aspect: [ tibaults_will, 25 ] minPower: 900 affixPool: - - [damage_reduction_from_close_enemies, 12] + - [ damage_reduction_from_close_enemies, 12 ] ``` +
-### Sigils -Sigils are all ok unless they match any of the blacklisted locations or affixes. See all sigil locations and affixes here: [assets/lang/enUS/sigils.json](assets/lang/enUS/sigils.json) -
Config Examples +Unique names are lower case and spaces are replaced by underscore. You can find the full list of names +in [assets/lang/enUS/uniques.json](assets/lang/enUS/uniques.json). -```yaml -Sigils: - minTier: 40 - maxTier: 100 - blacklist: - # locations - - endless_gates - - vault_of_the_forsaken +## Custom configs - # affixes - - armor_breakers - - resistance_breakers -``` -If you want to filter for a specific affix or location, you can also use the `whitelist` key. Even if `whitelist` is present, `blacklist` will be used to discard sigils that match any of the blacklisted affixes or locations. -```yaml -# Only keep sigils for vault_of_the_forsaken without the one or both of the affixes armor_breakers and resistance_breakers -Sigils: - minTier: 40 - maxTier: 100 - blacklist: - - armor_breakers - - resistance_breakers - whitelist: - - vault_of_the_forsaken -``` -
+D4LF will search for `params.ini` and for `profiles/*.yaml` in `C:/Users/WINDOWS_USER/.d4lf`. All values +in `C:/Users/WINDOWS_USER/.d4lf/params.ini` will overwrite the values from the `params.ini` file in the D4LF folder. In +the profiles folder, additional custom profiles can be added and used. -## Custom configs -D4LF will look for __params.ini__ and for __profiles/*.yaml__ also in C:/Users/WINDOWS_USER/.d4lf. All values in params.ini will overwrite the value from the param.ini in the D4LF folder. In the profiles folder additional custom profiles can be added and used. +This setup is helpful to facilitate updating to a new version as you don't need to copy around your config and profiles. -This is helpful to make it easier to update to a new version as you dont need to copy around your config and profiles. In case there are breaking changes to the configuration there will be a major release. E.g. update from 2.x.x -> 3.x.x. +**In the event of breaking changes to the configuration, there will be a major release, such as updating from 2.x.x to +3.x.x.** ## Develop ### Python Setup + - Install [miniconda](https://docs.conda.io/projects/miniconda/en/latest/) + ```bash git clone https://github.com/aeon0/d4lf cd d4lf @@ -223,12 +298,16 @@ python src/main.py ``` ### Linting + The CI will fail if the linter would change any files. You can run linting with: + ```bash conda activate d4lf black . ``` + To ignore certain code parts from formatting + ```python # fmt: off # ... @@ -237,9 +316,11 @@ To ignore certain code parts from formatting # fmt: skip # ... ``` + Setup VS Code by using the black formater extension. Also turn on "trim trailing whitespaces" is VS Code settings. ## Credits + - Icon based of: [CarbotAnimations](https://www.youtube.com/carbotanimations/about) - Some of the OCR code is originally from [@gleed](https://github.com/aliig). Good guy. - Names and textures for matching from [Blizzard](https://www.blizzard.com) diff --git a/config/params.ini b/config/params.ini index d147bf03..6e70c285 100644 --- a/config/params.ini +++ b/config/params.ini @@ -1,7 +1,7 @@ [general] ; Which filter profiles should be run. All .yaml files with "Aspects" and "Affixes" sections will be used from ; config/profiles/*.yaml and C:/Users/USERNAME/.d4lf/profiles/*.yaml -profiles=general,barb,druid,necro,rogue,sorc,uniques,sigils +profiles=example ; Whether to run vision mode on startup or not run_vision_mode_on_startup=True ; Which tabs to check. Note: All 6 Tabs must be unlocked! diff --git a/config/profiles/barb.yaml b/config/profiles/barb.yaml deleted file mode 100644 index a038e446..00000000 --- a/config/profiles/barb.yaml +++ /dev/null @@ -1,85 +0,0 @@ - -Affixes: - # HOTA barb - # ===================================== - - - Helm: - itemType: helm - minPower: 780 - affixPool: - - [cooldown_reduction] - - [total_armor] - - [maximum_life] - minAffixCount: 2 - - - Armor: - itemType: [chest armor, pants] - minPower: 780 - affixPool: - - [damage_reduction_from_close_enemies] - - [damage_reduction_while_fortified] - - [overpower_damage_with_twohanded_bludgeoning_weapons] - - [total_armor] - - [maximum_life] - - [damage_reduction_while_injured] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 780 - affixPool: - - [attack_speed] - - [ranks_of_hammer_of_the_ancients] - - [critical_strike_chance] - - [overpower_damage] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 780 - affixPool: - - [movement_speed] - - [fury_cost_reduction] - - [damage_reduction_while_injured] - minAffixCount: 3 - - - Amulet: - itemType: amulet - minPower: 780 - affixPool: - - [cooldown_reduction] - - [fury_cost_reduction] - - [ranks_of_the_counteroffensive_passive] - - [total_armor] - - [movement_speed] - minAffixCount: 3 - - - Ring: - itemType: ring - minPower: 780 - affixPool: - - [critical_strike_chance] - - [maximum_fury] - - [damage_while_berserking] - - [resource_generation] - minAffixCount: 3 - - - WeaponClose: - itemType: [two-handed mace, two-handed sword, two-handed axe] - minPower: 900 - affixPool: - - [damage_while_berserking] - - [overpower_damage] - - [all_stats] - - [strength] - minAffixCount: 3 - - - WeaponClose2: - itemType: [dagger, sword, axe, mace] - minPower: 900 - affixPool: - - [damage_while_berserking] - - [overpower_damage] - - [all_stats] - - [strength] - minAffixCount: 3 diff --git a/config/profiles/druid.yaml b/config/profiles/druid.yaml deleted file mode 100644 index fae92d21..00000000 --- a/config/profiles/druid.yaml +++ /dev/null @@ -1,80 +0,0 @@ -Aspects: - - [vigorous, 13] - - [overcharged, 15] - - [mighty_storms] - - -Affixes: - - Helm: - itemType: helm - minPower: 800 - affixPool: - - [basic_skill_attack_speed, 6] - - [cooldown_reduction, 5] - - [maximum_life, 580] - - [lightning_resistance, 50] - - [shadow_resistance, 50] - - [fire_resistance, 50] - - [poison_resistance, 50] - minAffixCount: 3 - - - Armor: - itemType: [chest armor, pants] - minPower: 800 - affixPool: - - [damage_reduction_while_fortified, 6] - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_poisoned_enemies, 6] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction, 5] - - [willpower, 35] - - [maximum_life, 700] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 800 - affixPool: - - [attack_speed, 7] - - [willpower, 35] - - [lucky_hit_chance, 4] - - [critical_strike_chance, 3] - - [storm_skill_cooldown_reduction, 5] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 800 - affixPool: - - [movement_speed, 15] - - [willpower, 35] - - [total_armor_while_in_werewolf_form, 15] - - [damage_reduction_while_injured, 25] - minAffixCount: 2 - - - Amulet: - itemType: amulet - minPower: 800 - affixPool: - - [cooldown_reduction, 5] - # - [damage_reduction, 6] - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction_from_poisoned_enemies, 6] - - [total_armor_while_in_werewolf_form, 15] - # - [total_armor, 9] - - [movement_speed, 12] - - [ranks_of_the_envenom_passive, 2] - minAffixCount: 2 - - - Ring: - itemType: ring - minPower: 800 - affixPool: - - [critical_strike_chance, 4] - - [lucky_hit_chance, 5] - - [damage_to_close_enemies, 18] - - [maximum_life, 680] - - [damage_reduction_from_distant_enemies, 12] - - [critical_strike_damage, 14] - minAffixCount: 3 diff --git a/config/profiles/example.yaml b/config/profiles/example.yaml new file mode 100644 index 00000000..80ffbacf --- /dev/null +++ b/config/profiles/example.yaml @@ -0,0 +1,88 @@ +Aspects: + - [ accelerating, 25 ] + - [ of_disobedience, 1.1 ] + - of_might + +Affixes: + - AwesomeHelm: + itemType: helm + minPower: 725 + affixPool: + - count: + - [ basic_skill_attack_speed, 6 ] + - [ cooldown_reduction, 5 ] + - [ maximum_life, 640 ] + - [ total_armor, 9 ] + minCount: 3 + + - AwesomeGloves: + itemType: gloves + minPower: 725 + affixPool: + - count: + - [ attack_speed, 7 ] + - [ lucky_hit_chance, 7.8 ] + - [ critical_strike_chance, 5.5 ] + + - AwesomeArmor: + itemType: [ chest armor, pants ] + minPower: 725 + affixPool: + - count: + - [ damage_reduction_from_close_enemies, 10 ] + - [ damage_reduction_from_distant_enemies, 12 ] + - [ damage_reduction, 5 ] + - [ total_armor, 9 ] + - [ maximum_life, 700 ] + - [ dodge_chance_against_close_enemies, 6.5 ] + - [ dodge_chance, 5.0 ] + minCount: 3 + + - AwesomeBoots: + itemType: boots + minPower: 725 + affixPool: + - count: + - [ movement_speed, 16 ] + - [ dodge_chance, 5 ] + - [ dodge_chance_against_distant_enemies, 7 ] + - [ energy_cost_reduction, 6 ] + minCount: 3 + + - AwesomeAmulet: + itemType: amulet + minPower: 725 + affixPool: + - count: + - [ cooldown_reduction, 6 ] + - [ damage_reduction, 6 ] + - [ damage_reduction_from_close_enemies, 10 ] + - [ damage_reduction_from_distant_enemies, 12 ] + - [ total_armor, 9 ] + - [ energy_cost_reduction, 6 ] + - [ movement_speed ] + minCount: 3 + + - AwesomeRing: + itemType: ring + minPower: 725 + affixPool: + - count: + - [ critical_strike_chance, 4 ] + - [ lucky_hit_chance, 5 ] + - [ resource_generation, 8 ] + - [ maximum_life, 680 ] + minCount: 3 + +Sigils: + minTier: 40 + maxTier: 100 + blacklist: + - endless_gates + - armor_breakers + - resistance_breakers + +Uniques: + - aspect: [ banished_lords_talisman ] + - aspect: [ fists_of_fate ] + - aspect: [ ring_of_the_ravenous ] \ No newline at end of file diff --git a/config/profiles/general.yaml b/config/profiles/general.yaml deleted file mode 100644 index 1f1e0c88..00000000 --- a/config/profiles/general.yaml +++ /dev/null @@ -1,95 +0,0 @@ -# find all aspect keys in "assets/aspects.json" -# Format is: [KEY, THRESHOLD, CONDITON] -# CONDITON can be "larger" or "smaller" and defaults to "larger" - -Aspects: - - [accelerating, 25] - - [of_might, 6.0] - - [of_disobedience, 1.1] - - [of_inner_calm, 10] - - [of_retribution, 20] - - [of_the_expectant, 10] - - [edgemasters, 20] - - [rapid, 30] - - [of_shared_misery, 45] - - [ghostwalker, 25] - - [of_the_umbral, 4] - - [conceited, 25] - - [starlight, 39] - - -# Find all affix keys in "config/affixes.json". Resource of possible affixes: https://d4builds.gg/database/gear-affixes/ - -# Format is: [KEY, THRESHOLD, CONDITON] -# CONDITON can be "larger" or "smaller" and defaults to "larger" - -# itemType must be any of: -# helm, chest armor, pants, gloves, boots, ring, amulet, axe, tow-handed axe, -# sword, two-handed sword, mace, two-handed mace, scythe, two-handed scythe, -# bow, bracers, crossbow, dagger, polarm, shield, staff, wand, offhand, totem - -Affixes: - - Helm: - itemType: helm - minPower: 725 - affixPool: - - [basic_skill_attack_speed, 6] - - [cooldown_reduction, 5] - - [maximum_life, 640] - - [total_armor, 9] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 725 - affixPool: - - [attack_speed, 7] - - [lucky_hit_chance, 7.8] - - [critical_strike_chance, 5.5] - minAffixCount: 3 - - - Armor: - itemType: [chest armor, pants] - minPower: 725 - affixPool: - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction, 5] - - [total_armor, 9] - - [maximum_life, 700] - - [dodge_chance_against_close_enemies, 6.5] - - [dodge_chance, 5.0] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 725 - affixPool: - - [movement_speed, 16] - - [dodge_chance, 5] - - [dodge_chance_against_distant_enemies, 7] - - [energy_cost_reduction, 6] - minAffixCount: 3 - - - Amulet: - itemType: amulet - minPower: 725 - affixPool: - - [cooldown_reduction, 6] - - [damage_reduction, 6] - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [total_armor, 9] - - [energy_cost_reduction, 6] - - [movement_speed] - minAffixCount: 3 - - - Ring: - itemType: ring - minPower: 725 - affixPool: - - [critical_strike_chance, 4] - - [lucky_hit_chance, 5] - - [resource_generation, 8] - - [maximum_life, 680] - minAffixCount: 3 diff --git a/config/profiles/necro.yaml b/config/profiles/necro.yaml deleted file mode 100644 index 4da0c3f5..00000000 --- a/config/profiles/necro.yaml +++ /dev/null @@ -1,104 +0,0 @@ -# Necro BloodSurge - -Aspects: - # General - - [of_shared_misery, 46] - - [of_disobedience, 1.0] - - [ghostwalker, 20] - - [of_the_umbral, 3] - # Necromancer - - [of_grasping_veins, 16] - - [of_shielding_storm, 3] - - [bloodbathed, 50] - - [of_rathmas_chosen, 45] - - [of_potent_blood, 16] - -Affixes: - - Chest: - itemType: [chest armor, pants] - minPower: 780 - affixPool: - - [maximum_life] - - [damage_reduction_from_close_enemies] - - [damage_reduction] - - [total_armor] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 780 - affixPool: - - [attack_speed] - - [ranks_of_blood_surge] - - [lucky_hit_chance] - - [critical_strike_chance] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - - [overpower_damage] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 780 - affixPool: - - [movement_speed] - - [ranks_of_corpse_tendrils] - - [essence_cost_reduction] - - [all_stats] - minAffixCount: 3 - - - Amulet: - itemType: amulet - minPower: 780 - affixPool: - - [cooldown_reduction] - - [damage_reduction] - - [total_armor] - - [essence_cost_reduction] - - [movement_speed] - - [ranks_of_the_tides_of_blood_passive] - minAffixCount: 3 - - - Ring: - itemType: ring - minPower: 780 - affixPool: - - [critical_strike_chance] - - [lucky_hit_chance] - - [overpower_damage] - - [maximum_life] - - [damage_to_close_enemies] - minAffixCount: 3 - - - WeaponClose: - itemType: dagger - minPower: 780 - affixPool: - - [overpower_damage] - - [core_skill_damage] - - [intelligence] - - [all_stats] - - [damage_to_close_enemies] - minAffixCount: 3 - - - Shield: - itemType: shield - minPower: 780 - affixPool: - - [maximum_life] - - [essence_cost_reduction] - - [cooldown_reduction] - - [damage_reduction_from_close_enemies] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource] - - [lucky_hit_chance] - minAffixCount: 3 - - - Helm: - itemType: helm - minPower: 780 - affixPool: - - [maximum_life] - - [total_armor] - - [cooldown_reduction] - - [basic_skill_attack_speed] - - [intelligence] - minAffixCount: 3 diff --git a/config/profiles/rogue.yaml b/config/profiles/rogue.yaml deleted file mode 100644 index 2ef611de..00000000 --- a/config/profiles/rogue.yaml +++ /dev/null @@ -1,217 +0,0 @@ -# Includes builds for Poison TB Rogue and Rapidshot / Penshot Rogue -# Its rather on the strict side - -Aspects: - # offensive - - [bladedancers, 15] - - [of_branching_volleys, 25] - - [trickshot, 20] - - [of_corruption, 40] - - [of_bursting_venoms, 10000] - - [repeating, 45] - - [of_pestilent_points, 150] - - # defensive - - [umbrous, 55] - - [cheats, 25] - - [enshrouding, 4] - - [of_elusive_menace, 7] - - # resource, utility - - [energizing, 9] - - [ravenous, 65] - - [of_noxious_ice, 29] - - [blasttrappers, 15] - - [frostbitten, 25] - - [manglers, 45] - - [assimilation, 10] - - # general - - [accelerating, 24] - - [of_might, 6] - - [of_disobedience, 1.0] - - [of_inner_calm, 10] - - [of_retribution, 20] - - [of_the_expectant, 10] - - [edgemasters, 20] - - [rapid, 28] - - [of_shared_misery, 45] - - [ghostwalker, 25] - - [of_the_umbral, 4] - - [conceited, 25] - - [starlight, 39] - - # - [snap_frozen] - # - [of_uncanny_treachery] - # - [of_lethal_dusk] - # - [icy_alchemists] - # - [toxic_alchemists] - # - [of_artful_initiative] - -Affixes: - - Helm: - itemType: helm - minPower: 800 - affixPool: - - [basic_skill_attack_speed, 7] - - [cooldown_reduction, 5] - - [dexterity, 36] - - [maximum_life, 640] - - [total_armor, 9] - - [ranks_of_poison_imbuement, 3] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 800 - affixPool: - - [attack_speed, 7] - - [dexterity, 38] - - [lucky_hit_chance, 7.8] - - [critical_strike_chance, 5.5] - - [ranks_of_twisting_blades, 3] - - [ranks_of_penetrating_shot, 3] - - [ranks_of_rapid_fire, 3] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 800 - affixPool: - - [movement_speed, 16] - - [dexterity, 40] - - [dodge_chance, 5] - - [energy_cost_reduction, 6] - - [dodge_chance_against_distant_enemies] - - [fire_resistance] - - [shadow_resistance] - - [poison_resistance] - - [lightning_resistance] - - [cold_resistance] - minAffixCount: 3 - - - Amulet: - itemType: amulet - minPower: 725 - affixPool: - - [cooldown_reduction, 6] - - [damage_reduction, 6] - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction_from_poisoned_enemies, 8] - - [total_armor, 9] - - [energy_cost_reduction, 6] - - [movement_speed] - - [ranks_of_all_imbuement_skills, 2] - - [ranks_of_the_exploit_passive, 2] - - [ranks_of_the_weapon_mastery_passive, 2] - - [ranks_of_the_malice_passive, 2] - - [ranks_of_the_frigid_finesse_passive, 2] - - [ranks_of_the_deadly_venom_passive, 2] - minAffixCount: 2 - - - Ring: - itemType: ring - minPower: 725 - affixPool: - - [critical_strike_chance, 4] - - [lucky_hit_chance, 5] - - [resource_generation, 8] - - [maximum_life, 680] - - [damage_to_crowd_controlled_enemies, 9] - minAffixCount: 3 - - - WeaponClose: - itemType: dagger - minPower: 915 - affixPool: - - [all_stats, 22] - - [dexterity, 44] - - [core_skill_damage, 17] - - [damage_to_close_enemies, 19] - - [vulnerable_damage, 16] - - [damage_to_slowed_enemies, 19] - - [damage_to_dazed_enemies, 19] - - [damage_to_crowd_controlled_enemies] - minAffixCount: 2 - - - WeaponFar: - itemType: [crossbow] - minPower: 725 - affixPool: - - [all_stats, 44] - - [dexterity, 90] - - [core_skill_damage, 25] - - [damage_to_close_enemies, 37] - - [vulnerable_damage, 34] - - [damage_to_slowed_enemies, 37] - - [damage_to_dazed_enemies, 37] - - [damage_to_crowd_controlled_enemies] - minAffixCount: 3 - - - Armor: - itemType: [chest armor, pants] - minPower: 725 - affixPool: - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction, 5] - - [total_armor, 9] - - [maximum_life, 700] - - [dodge_chance_against_close_enemies, 6.5] - - [dodge_chance, 5.0] - - [damage_reduction_from_poisoned_enemies, 8] - minAffixCount: 3 - -Uniques: - - aspect: [ashearas_khanjar, 6] - minPower: 900 - affixPool: - - [lucky_hit_chance, 4.5] - - - aspect: [condemnation] - minPower: 900 - - - aspect: [cowl_of_the_nameless, 23] - minPower: 825 - affixPool: - - [damage_reduction_from_close_enemies, 10] - - [ranks_of_all_imbuement_skills, 3] - - [cooldown_reduction, 6] - - - aspect: [eaglehorn, 78] - minPower: 900 - - - aspect: [skyhunter, 20] - minPower: 900 - affixPool: - - [ranks_of_the_exploit_passive, 2] - - - aspect: [windforce, 29] - minPower: 900 - - - aspect: [godslayer_crown, 58] - minPower: 825 - affixPool: - - [maximum_life, 680] - - - aspect: [penitent_greaves, 10] - affixPool: - - [movement_speed, 16] - - - aspect: [tibaults_will, 30] - affixPool: - - [damage_reduction_from_close_enemies, 10] - - - aspect: [xfals_corroded_signet] - affixPool: - - [lucky_hit_chance, 5] - - - aspect: [fists_of_fate, 280] - - aspect: [frostburn, 22] - - - aspect: [andariels_visage] - - aspect: [doombringer] - - aspect: [harlequin_crest] - - aspect: [melted_heart_of_selig] - - aspect: [ring_of_starless_skies] diff --git a/config/profiles/sigils.yaml b/config/profiles/sigils.yaml deleted file mode 100644 index 4192af07..00000000 --- a/config/profiles/sigils.yaml +++ /dev/null @@ -1,11 +0,0 @@ -Sigils: - minTier: 40 - maxTier: 100 - blacklist: - # locations - - endless_gates - - vault_of_the_forsaken - - # affixes - - armor_breakers - - resistance_breakers diff --git a/config/profiles/sorc.yaml b/config/profiles/sorc.yaml deleted file mode 100644 index c5d43bd2..00000000 --- a/config/profiles/sorc.yaml +++ /dev/null @@ -1,109 +0,0 @@ -# Ball Lightning Sorcerer Endgame -Aspects: - - [magelords, 8] - - [of_the_unwavering, 5] - - [gravitational, 25] - - [recharging, 2.5] - - [storm_swell, 25] - - [everliving, 24] - - [of_disobedience] - - [of_fortune, 19] - - [elementalists, 38] - - [accelerating, 24] - -Affixes: - - Helm: - itemType: helm - minPower: 725 - affixPool: - - [cooldown_reduction, 6] - - [lucky_hit_chance_while_you_have_a_barrier,8] - - [maximum_life, 580] - - [total_armor, 9] - - [maximum_mana, 10] - minAffixCount: 3 - - - Armor: - itemType: [chest armor, pants] - minPower: 725 - affixPool: - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [damage_reduction_from_burning_enemies, 10] - - [damage_reduction, 5] - - [total_armor, 9] - - [maximum_life, 580] - - [ranks_of_ball_lightning, 3] - minAffixCount: 3 - - - Amulet: - itemType: amulet - minPower: 725 - affixPool: - - [cooldown_reduction, 6] - - [mana_cost_reduction, 10] - - [total_armor, 9] - - [ranks_of_all_mastery_skills, 2] - - [damage_reduction, 6.7] - - [movement_speed, 14] - - [lucky_hit_chance_while_you_have_a_barrier, 8] - - [damage_reduction_from_close_enemies, 10] - - [damage_reduction_from_distant_enemies, 12] - - [movement_speed, 14] - minAffixCount: 3 - - - Ring: - itemType: ring - minPower: 725 - affixPool: - - [critical_strike_chance, 3] - - [damage_to_close_enemies, 17] - - [resource_generation, 6] - - [lucky_hit_chance, 5] - - [maximum_life, 580] - - [maximum_mana, 10] - minAffixCount: 3 - - - WeaponClose: - itemType: [dagger] - minPower: 890 - affixPool: - - [all_stats, 20] - - [intelligence, 38] - - [damage_to_close_enemies, 16.5] - - [weapon_mastery_skill_damage, 12.5] - - [vulnerable_damage, 16] - minAffixCount: 3 - - - Focus: - itemType: [focus] - minPower: 890 - affixPool: - - [cooldown_reduction, 4] - - [resource_generation, 5] - - [critical_strike_chance, 2] - - [mana_cost_reduction, 4] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource, 10] - - [lucky_hit_chance, 5] - - [lucky_hit_chance_while_you_have_a_barrier, 5] - minAffixCount: 3 - - - Boots: - itemType: boots - minPower: 725 - affixPool: - - [cooldown_reduction, 6] - - [mana_cost_reduction, 10] - - [intelligence, 35] - - [movement_speed, 14] - minAffixCount: 3 - - - Gloves: - itemType: gloves - minPower: 725 - affixPool: - - [critical_strike_chance, 4] - - [attack_speed, 9] - - [lucky_hit_up_to_a_chance_to_restore_primary_resource, 10] - - [lucky_hit_chance, 5] - minAffixCount: 3 diff --git a/config/profiles/uniques.yaml b/config/profiles/uniques.yaml deleted file mode 100644 index 9544baaf..00000000 --- a/config/profiles/uniques.yaml +++ /dev/null @@ -1,89 +0,0 @@ -# If an unique is not included in any filter profile it will be discarded! -# Example to filter for specific values on Uniques - -# - aspect: [tibaults_will, 38] -# minPower: 900 -# affixPool: -# - [damage_reduction_from_close_enemies, 12] - -# This will take Tibaults Will only with >= 38%[x] on the aspect, itemPower >= 900 and dmg reduction close >= 12% - -Uniques: - - aspect: [banished_lords_talisman] - - aspect: [fists_of_fate] - - aspect: [flickerstep] - - aspect: [frostburn] - - aspect: [godslayer_crown] - - aspect: [mothers_embrace] - - aspect: [penitent_greaves] - - aspect: [razorplate] - - aspect: [soulbrand] - - aspect: [tassets_of_the_dawning_sky] - - aspect: [temerity] - - aspect: [the_butchers_cleaver] - - aspect: [tibaults_will] - - aspect: [xfals_corroded_signet] - - aspect: [ahavarion_spear_of_lycander] - - aspect: [andariels_visage] - - aspect: [doombringer] - - aspect: [harlequin_crest] - - aspect: [melted_heart_of_selig] - - aspect: [ring_of_starless_skies] - - aspect: [the_grandfather] - - aspect: [100000_steps] - - aspect: [ancients_oath] - - aspect: [azurewrath] - - aspect: [battle_trance] - - aspect: [fields_of_crimson] - - aspect: [gohrs_devastating_grips] - - aspect: [hellhammer] - - aspect: [overkill] - - aspect: [rage_of_harrogath] - - aspect: [ramaladnis_magnum_opus] - - aspect: [ring_of_red_furor] - - aspect: [tuskhelm_of_joritz_the_mighty] - - aspect: [airidahs_inexorable_will] - - aspect: [dolmen_stone] - - aspect: [fleshrender] - - aspect: [greatstaff_of_the_crone] - - aspect: [hunters_zenith] - - aspect: [insatiable_fury] - - aspect: [mad_wolfs_glee] - - aspect: [storms_companion] - - aspect: [tempest_roar] - - aspect: [vasilys_prayer] - - aspect: [waxing_gibbous] - - aspect: [black_river] - - aspect: [blood_artisans_cuirass] - - aspect: [blood_moon_breeches] - - aspect: [bloodless_scream] - - aspect: [deathless_visage] - - aspect: [deathspeakers_pendant] - - aspect: [greaves_of_the_empty_tomb] - - aspect: [howl_from_below] - - aspect: [lidless_wall] - - aspect: [ring_of_mendeln] - - aspect: [ring_of_the_sacrilegious_soul] - - aspect: [ashearas_khanjar] - - aspect: [condemnation] - - aspect: [cowl_of_the_nameless] - - aspect: [eaglehorn] - - aspect: [eyes_in_the_dark] - - aspect: [grasp_of_shadow] - - aspect: [scoundrels_leathers] - - aspect: [skyhunter] - - aspect: [windforce] - - aspect: [word_of_hakan] - - aspect: [writhing_band_of_trickery] - - aspect: [blue_rose] - - aspect: [esadoras_overflowing_cameo] - - aspect: [esus_heirloom] - - aspect: [flamescar] - - aspect: [gloves_of_the_illuminator] - - aspect: [iceheart_brais] - - aspect: [raiment_of_the_infinite] - - aspect: [staff_of_endless_rage] - - aspect: [staff_of_lam_esen] - - aspect: [tal_rashas_iridescent_loop] - - aspect: [the_oculus] - - aspect: [ring_of_the_ravenous] diff --git a/src/config/models.py b/src/config/models.py index 195cb6bd..1b49b922 100644 --- a/src/config/models.py +++ b/src/config/models.py @@ -1,11 +1,11 @@ """New config loading and verification using pydantic. For now, both will exist in parallel hence _new.""" import enum +import sys from pathlib import Path -from typing import List import numpy -from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, field_validator, model_validator, RootModel from pydantic_numpy import np_array_pydantic_annotated_typing from pydantic_numpy.model import NumpyModel @@ -22,19 +22,17 @@ class _IniBaseModel(BaseModel): model_config = ConfigDict(frozen=True, str_strip_whitespace=True, str_to_lower=True) +def _parse_item_type(data: str | list[str]) -> list[str]: + if isinstance(data, str): + return [data] + return data + + class AffixAspectFilterModel(BaseModel): name: str value: float | None = None comparison: ComparisonType = ComparisonType.larger - @field_validator("name") - def name_must_exist(cls, name: str) -> str: - import dataloader # This on module level would be a circular import, so we do it lazy for now - - if name not in dataloader.Dataloader().affix_dict.keys() and name not in dataloader.Dataloader().aspect_unique_dict.keys(): - raise ValueError(f"affix {name} does not exist") - return name - @model_validator(mode="before") def parse_data(cls, data: str | list[str] | list[str | float] | dict[str, str | float]) -> dict[str, str | float]: if isinstance(data, dict): @@ -55,6 +53,50 @@ def parse_data(cls, data: str | list[str] | list[str | float] | dict[str, str | raise ValueError("must be str or list") +class AffixFilterModel(AffixAspectFilterModel): + @field_validator("name") + def name_must_exist(cls, name: str) -> str: + import dataloader # This on module level would be a circular import, so we do it lazy for now + + if name not in dataloader.Dataloader().affix_dict.keys(): + raise ValueError(f"affix {name} does not exist") + return name + + +class AffixFilterCountModel(BaseModel): + count: list[AffixFilterModel] = [] + maxCount: int = 5 + minCount: int = 1 + + @model_validator(mode="before") + def set_defaults(cls, data: "AffixFilterCountModel") -> "AffixFilterCountModel": + if "minCount" not in data and "count" in data and isinstance(data["count"], list): + data["minCount"] = len(data["count"]) + if "maxCount" not in data and "count" in data and isinstance(data["count"], list): + data["maxCount"] = len(data["count"]) + return data + + +class AspectFilterModel(AffixAspectFilterModel): + @field_validator("name") + def name_must_exist(cls, name: str) -> str: + import dataloader # This on module level would be a circular import, so we do it lazy for now + + if name not in dataloader.Dataloader().aspect_dict.keys(): + raise ValueError(f"affix {name} does not exist") + return name + + +class AspectUniqueFilterModel(AffixAspectFilterModel): + @field_validator("name") + def name_must_exist(cls, name: str) -> str: + import dataloader # This on module level would be a circular import, so we do it lazy for now + + if name not in dataloader.Dataloader().aspect_unique_dict.keys(): + raise ValueError(f"affix {name} does not exist") + return name + + class AdvancedOptionsModel(_IniBaseModel): exit_key: str log_lvl: str = "info" @@ -101,7 +143,7 @@ class ColorsModel(_IniBaseModel): unusable_red: "HSVRangeModel" -class General(_IniBaseModel): +class GeneralModel(_IniBaseModel): check_chest_tabs: list[int] hidden_transparency: float language: str = "enUS" @@ -166,9 +208,23 @@ def values_in_range(cls, v: numpy.ndarray) -> numpy.ndarray: return v +class ItemFilterModel(BaseModel): + affixPool: list[AffixFilterCountModel] = [] + inherentPool: list[AffixFilterCountModel] = [] + itemType: list[ItemType] = [] + minPower: int = 0 + + @field_validator("itemType", mode="before") + def parse_item_type(cls, data: str | list[str]) -> list[str]: + return _parse_item_type(data) + + +DynamicItemFilterModel = RootModel[dict[str, ItemFilterModel]] + + class SigilModel(BaseModel): minTier: int = 0 - maxTier: int = 100 + maxTier: int = sys.maxsize blacklist: list[str] = [] whitelist: list[str] = [] @@ -205,22 +261,22 @@ def name_must_exist(cls, names: list[str]) -> list[str]: class UniqueModel(BaseModel): - affix: list[AffixAspectFilterModel] = [] - aspect: AffixAspectFilterModel = None + affix: list[AffixFilterModel] = [] + aspect: AspectUniqueFilterModel = None itemType: list[ItemType] = [] minPower: int = 0 @field_validator("itemType", mode="before") def parse_item_type(cls, data: str | list[str]) -> list[str]: - if isinstance(data, str): - return [data] - return data + return _parse_item_type(data) class ProfileModel(BaseModel): name: str + Affixes: list[DynamicItemFilterModel] = [] + Aspects: list[AspectFilterModel] = [] Sigils: SigilModel | None = None - Uniques: List[UniqueModel] = [] + Uniques: list[UniqueModel] = [] class UiOffsetsModel(_IniBaseModel): diff --git a/src/dataloader.py b/src/dataloader.py index 454d6051..74b39219 100644 --- a/src/dataloader.py +++ b/src/dataloader.py @@ -1,4 +1,5 @@ import json +import os import threading from pathlib import Path @@ -32,31 +33,23 @@ def __new__(cls): return cls._instance def load_data(self): - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/affixes.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/affixes.json", "r", encoding="utf-8") as f: self.affix_dict: dict = json.load(f) - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/aspects.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/aspects.json", "r", encoding="utf-8") as f: data = json.load(f) for key, d in data.items(): # Note: If you adjust the :68, also adjust it in find_aspect.py self.aspect_dict[key] = d["desc"][:68] self.aspect_num_idx[key] = d["num_idx"] - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/corrections.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/corrections.json", "r", encoding="utf-8") as f: data = json.load(f) self.error_map = data["error_map"] self.filter_after_keyword = data["filter_after_keyword"] self.filter_words = data["filter_words"] - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/item_types.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/item_types.json", "r", encoding="utf-8") as f: data = json.load(f) for item, value in data.items(): if item in ItemType.__members__: @@ -65,9 +58,7 @@ def load_data(self): else: Logger.warning(f"{item} type not in item_type.py") - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/sigils.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/sigils.json", "r", encoding="utf-8") as f: affix_sigil_dict_all = json.load(f) self.affix_sigil_dict = { **affix_sigil_dict_all["dungeons"], @@ -76,14 +67,10 @@ def load_data(self): **affix_sigil_dict_all["positive"], } - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/tooltips.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/tooltips.json", "r", encoding="utf-8") as f: self.tooltips = json.load(f) - with open( - Path(__file__).parent.parent / f"assets/lang/{IniConfigLoader().general.language}/uniques.json", "r", encoding="utf-8" - ) as f: + with open(Path(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/uniques.json", "r", encoding="utf-8") as f: data = json.load(f) for key, d in data.items(): # Note: If you adjust the :45, also adjust it in find_aspect.py diff --git a/src/item/data/aspect.py b/src/item/data/aspect.py index 3a3c5a72..6310a835 100644 --- a/src/item/data/aspect.py +++ b/src/item/data/aspect.py @@ -3,7 +3,7 @@ @dataclass class Aspect: - type: str | None + type: str value: float = None text: str = "" loc: tuple[int, int] = None diff --git a/src/item/descr/item_type.py b/src/item/descr/item_type.py index d5ca0d6e..7d3e25cb 100644 --- a/src/item/descr/item_type.py +++ b/src/item/descr/item_type.py @@ -28,29 +28,29 @@ def read_item_type( # TODO: Language specific if "sigil" in concatenated_str and Dataloader().tooltips["ItemTier"] in concatenated_str: # process sigil - item.type = ItemType.Sigil + item.item_type = ItemType.Sigil elif rarity in [ItemRarity.Common, ItemRarity.Legendary]: # We check if it is a material mask, _ = color_filter(crop_top, COLORS.material_color, False) mean_val = np.mean(mask) if mean_val > 2.0: - item.type = ItemType.Material + item.item_type = ItemType.Material return item, concatenated_str elif rarity == ItemRarity.Common: return item, concatenated_str - if item.type == ItemType.Sigil: + if item.item_type == ItemType.Sigil: item.power = _find_sigil_tier(concatenated_str) else: item = _find_item_power_and_type(item, concatenated_str) - if item.type is None: + if item.item_type is None: masked_red, _ = color_filter(crop_top, COLORS.unusable_red, False) crop_top[masked_red == 255] = [235, 235, 235] concatenated_str = image_to_text(crop_top).text.lower().replace("\n", " ") item = _find_item_power_and_type(item, concatenated_str) - non_magic_or_sigil = item.rarity != ItemRarity.Magic or item.type == ItemType.Sigil - power_or_type_bad = item.power is None or item.type is None + non_magic_or_sigil = item.rarity != ItemRarity.Magic or item.item_type == ItemType.Sigil + power_or_type_bad = item.power is None or item.item_type is None if non_magic_or_sigil and power_or_type_bad: return None, concatenated_str @@ -77,28 +77,27 @@ def _find_item_power_and_type(item: Item, concatenated_str: str) -> Item: if (found_idx := concatenated_str.rfind(item_type.value)) != -1: tmp_idx = found_idx + len(item_type.value) if tmp_idx >= last_char_idx and len(item_type.value) > max_length: - item.type = item_type + item.item_type = item_type last_char_idx = tmp_idx max_length = len(item_type.value) # common mistake is that "Armor" is on a seperate line and can not be detected properly # TODO: Language specific - if item.type is None: + if item.item_type is None: if "chest" in concatenated_str or "armor" in concatenated_str: - item.type = ItemType.ChestArmor + item.item_type = ItemType.ChestArmor if "two-handed" in concatenated_str or "two- handed" in concatenated_str: - if item.type == ItemType.Sword: - item.type = ItemType.Sword2H - elif item.type == ItemType.Mace: - item.type = ItemType.Mace2H - elif item.type == ItemType.Scythe: - item.type = ItemType.Scythe2H - elif item.type == ItemType.Axe: - item.type = ItemType.Axe2H + if item.item_type == ItemType.Sword: + item.item_type = ItemType.Sword2H + elif item.item_type == ItemType.Mace: + item.item_type = ItemType.Mace2H + elif item.item_type == ItemType.Scythe: + item.item_type = ItemType.Scythe2H + elif item.item_type == ItemType.Axe: + item.item_type = ItemType.Axe2H return item def _find_sigil_tier(concatenated_str: str) -> int: - idx = None for error, correction in Dataloader().error_map.items(): concatenated_str = concatenated_str.replace(error, correction) if Dataloader().tooltips["ItemTier"] in concatenated_str: diff --git a/src/item/descr/read_descr.py b/src/item/descr/read_descr.py index e0b18ac2..cb053e96 100644 --- a/src/item/descr/read_descr.py +++ b/src/item/descr/read_descr.py @@ -36,7 +36,7 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo screenshot("failed_itempower_itemtype", img=img_item_descr) return None - if item.type == ItemType.Material or (item.rarity in [ItemRarity.Magic, ItemRarity.Common] and item.type != ItemType.Sigil): + if item.item_type == ItemType.Material or (item.rarity in [ItemRarity.Magic, ItemRarity.Common] and item.item_type != ItemType.Sigil): return item # Find textures for bullets and sockets @@ -47,15 +47,15 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo # Split affix bullets into inherent and others # ========================= - if item.type in [ItemType.ChestArmor, ItemType.Helm, ItemType.Gloves]: + if item.item_type in [ItemType.ChestArmor, ItemType.Helm, ItemType.Gloves]: inhernet_affixe_bullets = [] - elif item.type in [ItemType.Ring]: + elif item.item_type in [ItemType.Ring]: inhernet_affixe_bullets = affix_bullets[:2] affix_bullets = affix_bullets[2:] - elif item.type in [ItemType.Sigil]: + elif item.item_type in [ItemType.Sigil]: inhernet_affixe_bullets = affix_bullets[:3] affix_bullets = affix_bullets[3:] - elif item.type in [ItemType.Shield]: + elif item.item_type in [ItemType.Shield]: inhernet_affixe_bullets = affix_bullets[:4] affix_bullets = affix_bullets[4:] else: @@ -65,7 +65,7 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo # Find inherent affixes # ========================= - is_sigil = item.type == ItemType.Sigil + is_sigil = item.item_type == ItemType.Sigil line_height = ResManager().offsets.item_descr_line_height if len(inhernet_affixe_bullets) > 0 and len(affix_bullets) > 0: bottom_limit = affix_bullets[0].center[1] - int(line_height // 2) @@ -98,9 +98,9 @@ def read_descr(rarity: ItemRarity, img_item_descr: np.ndarray, show_warnings: bo # Find aspects & uniques # ========================= if rarity in [ItemRarity.Legendary, ItemRarity.Unique]: - item.aspect, debug_str = find_aspect(img_item_descr, aspect_bullet, item.type, item.rarity) + item.aspect, debug_str = find_aspect(img_item_descr, aspect_bullet, item.item_type, item.rarity) if item.aspect is None: - item.aspect, debug_str = find_aspect(img_item_descr, aspect_bullet, item.type, item.rarity, False) + item.aspect, debug_str = find_aspect(img_item_descr, aspect_bullet, item.item_type, item.rarity, False) if item.aspect is None: if show_warnings: Logger.warning(f"Could not find aspect/unique: {debug_str}") diff --git a/src/item/filter.py b/src/item/filter.py index f50a305f..2fc3c08f 100644 --- a/src/item/filter.py +++ b/src/item/filter.py @@ -5,10 +5,20 @@ from pathlib import Path import yaml +from pydantic import ValidationError from config.loader import IniConfigLoader -from config.models import ProfileModel, UniqueModel, SigilModel, AffixAspectFilterModel, ComparisonType -from dataloader import Dataloader +from config.models import ( + ProfileModel, + UniqueModel, + SigilModel, + AffixAspectFilterModel, + ComparisonType, + AffixFilterModel, + AspectFilterModel, + DynamicItemFilterModel, + AffixFilterCountModel, +) from item.data.affix import Affix from item.data.aspect import Aspect from item.data.item_type import ItemType @@ -48,45 +58,156 @@ def __new__(cls): cls._instance = super(Filter, cls).__new__(cls) return cls._instance - @staticmethod - def _check_item_types(filters): - for filter_dict in filters: - for filter_name, filter_data in filter_dict.items(): - user_item_types = [filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"] - if user_item_types is None: - Logger.warning(f"Warning: Missing itemtype in {filter_name}") + def _check_affixes(self, item: Item) -> FilterResult: + res = FilterResult(False, []) + if not self.affix_filters: + return FilterResult(True, []) + for profile_name, profile_filter in self.affix_filters.items(): + for filter_item in profile_filter: + filter_name = next(iter(filter_item.root.keys())) + filter_spec = filter_item.root[filter_name] + # check item type + if not self._match_item_type(expected_item_types=filter_spec.itemType, item_type=item.item_type): + continue + # check item power + if not self._match_item_power(min_power=filter_spec.minPower, item_power=item.power): + continue + # check affixes + matched_affixes = [] + if filter_spec.affixPool: + matched_affixes = self._match_affixes_count(expected_affixes=filter_spec.affixPool, item_affixes=item.affixes) + if not matched_affixes: + continue + # check inherent + matched_inherents = [] + if filter_spec.inherentPool: + matched_inherents = self._match_affixes_count(expected_affixes=filter_spec.inherentPool, item_affixes=item.inherent) + if not matched_inherents: + continue + all_matches = matched_affixes + matched_inherents + Logger.info(f"Matched {profile_name}.Affixes.{filter_name}: {all_matches}") + res.keep = True + res.matched.append(MatchedFilter(f"{profile_name}.{filter_name}", all_matches)) + return res + + def _check_aspect(self, item: Item) -> FilterResult: + res = FilterResult(False, []) + if not self.aspect_filters: + return FilterResult(True, []) + for profile_name, profile_filter in self.aspect_filters.items(): + for filter_item in profile_filter: + if not self._match_item_aspect_or_affix(expected_aspect=filter_item, item_aspect=item.aspect): + continue + Logger.info(f"Matched {profile_name}.Aspects: [{item.aspect.type}, {item.aspect.value}]") + res.keep = True + res.matched.append(MatchedFilter(f"{profile_name}.Aspects", did_match_aspect=True)) + return res + + def _check_sigil(self, item: Item) -> FilterResult: + res = FilterResult(False, []) + if not self.sigil_filters: + return FilterResult(True, []) + for profile_name, profile_filter in self.sigil_filters.items(): + # check item power + if not self._match_item_power(max_power=profile_filter.maxTier, min_power=profile_filter.minTier, item_power=item.power): + continue + # check affix blacklist + if profile_filter.blacklist and self._match_affixes_sigils( + expected_affixes=profile_filter.blacklist, sigil_affixes=item.affixes + item.inherent + ): + continue + # check affix whitelist + if profile_filter.whitelist and not self._match_affixes_sigils( + expected_affixes=profile_filter.whitelist, sigil_affixes=item.affixes + item.inherent + ): + continue + Logger.info(f"Matched {profile_name}.Sigils") + res.keep = True + res.matched.append(MatchedFilter(f"{profile_name}")) + return res + + def _check_unique_item(self, item: Item) -> FilterResult: + res = FilterResult(False, []) + if not self.unique_filters: + return FilterResult(True, []) + for profile_name, profile_filter in self.unique_filters.items(): + for filter_item in profile_filter: + # check item type + if not self._match_item_type(expected_item_types=filter_item.itemType, item_type=item.item_type): continue - invalid_types = [] - for val in user_item_types: - try: - ItemType(val) - except ValueError: - invalid_types.append(val) - if invalid_types: - Logger.warning(f"Warning: Invalid ItemTypes in filter {filter_name}: {', '.join(invalid_types)}") + # check item power + if not self._match_item_power(min_power=filter_item.minPower, item_power=item.power): + continue + # check aspect + if not self._match_item_aspect_or_affix(expected_aspect=filter_item.aspect, item_aspect=item.aspect): + continue + # check affixes + if not self._match_affixes_uniques(expected_affixes=filter_item.affix, item_affixes=item.affixes): + continue + Logger.info(f"Matched {profile_name}.Uniques: {item.aspect.type}") + res.keep = True + res.matched.append(MatchedFilter(f"{profile_name}.{item.aspect.type}", did_match_aspect=True)) + return res + + def _did_files_change(self) -> bool: + if self.last_loaded is None: + return True + return any(os.path.getmtime(file_path) > self.last_loaded for file_path in self.all_file_pathes) + + def _match_affixes_count(self, expected_affixes: list[AffixFilterCountModel], item_affixes: list[Affix]) -> list[str]: + result = [] + for count_group in expected_affixes: + group_res = [] + for affix in count_group.count: + matched_item_affix = next((a for a in item_affixes if a.type == affix.name), None) + if matched_item_affix is not None and self._match_item_aspect_or_affix(affix, matched_item_affix): + group_res.append(affix.name) + if count_group.minCount <= len(group_res) <= count_group.maxCount: + result.extend(group_res) + else: # if one group fails, everything fails + return [] + return result @staticmethod - def _check_affix_pool(affix_pool, affix_dict, filter_name): - user_affix_pool = affix_pool - invalid_affixes = [] - if user_affix_pool is None: - return - for affix in user_affix_pool: - if isinstance(affix, dict) and "any_of" in affix: - affix_list = affix["any_of"] if affix["any_of"] is not None else [] - else: - affix_list = [affix] - for a in affix_list: - affix_name = a if isinstance(a, str) else a[0] - if affix_name not in affix_dict: - invalid_affixes.append(affix_name) - if invalid_affixes: - Logger.warning(f"Warning: Invalid Affixes in filter {filter_name}: {', '.join(invalid_affixes)}") + def _match_affixes_sigils(expected_affixes: list[str], sigil_affixes: list[Affix]) -> bool: + return any(a.type in expected_affixes for a in sigil_affixes) + + def _match_affixes_uniques(self, expected_affixes: list[AffixFilterModel], item_affixes: list[Affix]) -> bool: + for expected_affix in expected_affixes: + matched_item_affix = next((a for a in item_affixes if a.type == expected_affix.name), None) + if matched_item_affix is None or not self._match_item_aspect_or_affix(expected_affix, matched_item_affix): + return False + return True + + @staticmethod + def _match_item_aspect_or_affix(expected_aspect: AffixAspectFilterModel | None, item_aspect: Aspect | Affix) -> bool: + if expected_aspect is None: + return True + if expected_aspect.name != item_aspect.type: + return False + if expected_aspect.value is not None: + if item_aspect.value is None: + return False + if (expected_aspect.comparison == ComparisonType.larger and item_aspect.value <= expected_aspect.value) or ( + expected_aspect.comparison == ComparisonType.smaller and item_aspect.value >= expected_aspect.value + ): + return False + return True + + @staticmethod + def _match_item_power(min_power: int, item_power: int, max_power: int = sys.maxsize) -> bool: + return min_power <= item_power <= max_power + + @staticmethod + def _match_item_type(expected_item_types: list[ItemType], item_type: ItemType) -> bool: + if not expected_item_types: + return True + return item_type in expected_item_types def load_files(self): self.files_loaded = True - self.affix_filters = dict() - self.aspect_filters = dict() + self.affix_filters: dict[str, list[DynamicItemFilterModel]] = dict() + self.aspect_filters: dict[str, list[AspectFilterModel]] = dict() self.sigil_filters: dict[str, SigilModel] = dict() self.unique_filters: dict[str, list[UniqueModel]] = dict() profiles: list[str] = IniConfigLoader().general.profiles @@ -96,6 +217,7 @@ def load_files(self): params_profile_path = Path(f"config/profiles") self.all_file_pathes = [] + errors = False for profile_str in profiles: custom_file_path = custom_profile_path / f"{profile_str}.yaml" params_file_path = params_profile_path / f"{profile_str}.yaml" @@ -121,244 +243,61 @@ def load_files(self): except Exception as e: Logger.error(f"An unexpected error occurred loading YAML file {profile_path}: {e}") continue + if config is None: + Logger.error(f"Empty YAML file {profile_path}, please remove it") + continue info_str = f"Loading profile {profile_str}: " + try: + data = ProfileModel(name=profile_str, **config) + except ValidationError as e: + errors = True + Logger.error(f"Validation errors in {profile_path}") + Logger.error(e) + continue + self.affix_filters[data.name] = data.Affixes + self.aspect_filters[data.name] = data.Aspects + self.sigil_filters[data.name] = data.Sigils + self.unique_filters[data.name] = data.Uniques - if config is not None and "Affixes" in config: + if data.Affixes: info_str += "Affixes " - if config["Affixes"] is None: - Logger.error(f"Empty Affixes section in {profile_str}. Remove it") - return - self.affix_filters[profile_str] = config["Affixes"] - # Sanity check on the item types - self._check_item_types(self.affix_filters[profile_str]) - # Sanity check on the affixes - for filter_dict in self.affix_filters[profile_str]: - for filter_name, filter_data in filter_dict.items(): - if "affixPool" in filter_data: - self._check_affix_pool(filter_data["affixPool"], Dataloader().affix_dict, filter_name) - else: - filter_data["affixPool"] = [] - if "inherentPool" in filter_data: - self._check_affix_pool(filter_data["inherentPool"], Dataloader().affix_dict, filter_name) - - if config is not None and "Sigils" in config: - info_str += "Sigils " - data = ProfileModel(name=profile_str, **config) - if data.Sigils is None: - Logger.error(f"Empty Sigils section in {profile_str}. Remove it") - return - self.sigil_filters[data.name] = data.Sigils - if config is not None and "Aspects" in config: + if data.Aspects: info_str += "Aspects " - if config["Aspects"] is None: - Logger.error(f"Empty Aspects section in {profile_str}. Remove it") - return - self.aspect_filters[profile_str] = config["Aspects"] - invalid_aspects = [] - for aspect in self.aspect_filters[profile_str]: - aspect_name = aspect if isinstance(aspect, str) else aspect[0] - if aspect_name not in Dataloader().aspect_dict: - invalid_aspects.append(aspect_name) - if invalid_aspects: - Logger.warning(f"Warning: Invalid Aspect: {', '.join(invalid_aspects)}") - - if config is not None and "Uniques" in config: + + if data.Sigils: + info_str += "Sigils " + + if data.Uniques: info_str += "Uniques" - data = ProfileModel(name=profile_str, **config) - if not data.Uniques: - Logger.error(f"Empty Uniques section in {profile_str}. Remove it") - return - self.unique_filters[data.name] = data.Uniques Logger.info(info_str) - + if errors: + Logger.error("Errors occurred while loading profiles, please check the log for details") + sys.exit(1) self.last_loaded = time.time() - def _did_files_change(self) -> bool: - if self.last_loaded is None: - return True - return any(os.path.getmtime(file_path) > self.last_loaded for file_path in self.all_file_pathes) - - @staticmethod - def _check_item_aspect(expected_aspect: AffixAspectFilterModel, item_aspect: Aspect) -> bool: - if expected_aspect.name != item_aspect.type: - return False - if expected_aspect.value is not None: - if item_aspect.value is None: - return False - if not (expected_aspect.comparison == ComparisonType.larger and item_aspect.value >= expected_aspect.value) or ( - expected_aspect.comparison == ComparisonType.smaller and item_aspect.value <= expected_aspect.value - ): - return False - return True - - @staticmethod - def _check_item_power(min_power: int, item_power: int, max_power: int = sys.maxsize) -> bool: - return min_power <= item_power <= max_power - - @staticmethod - def _check_item_type(expected_item_types: list[ItemType], item_type: ItemType) -> bool: - return item_type in expected_item_types - - def _match_affixes(self, filter_affix_pool: list, item_affix_pool: list[Affix]) -> list: - item_affix_pool = item_affix_pool[:] - matched_affixes = [] - if filter_affix_pool is None: - return matched_affixes - filter_affix_pool = [filter_affix_pool] if isinstance(filter_affix_pool, str) else filter_affix_pool - - for affix in filter_affix_pool: - if isinstance(affix, dict) and "any_of" in affix: - any_of_matched = self._match_affixes(affix["any_of"], item_affix_pool) - if len(any_of_matched) > 0: - name = any_of_matched[0] - item_affix_pool = [a for a in item_affix_pool if a.type != name] - matched_affixes.append(name) - else: - name, *rest = affix if isinstance(affix, list) else [affix] - threshold = rest[0] if rest else None - condition = rest[1] if len(rest) > 1 else "larger" - - item_affix_value = next((a.value for a in item_affix_pool if a.type == name), None) - if item_affix_value is not None: - if ( - threshold is None - or (isinstance(condition, str) and condition == "larger" and item_affix_value >= threshold) - or (isinstance(condition, str) and condition == "smaller" and item_affix_value <= threshold) - ): - item_affix_pool = [a for a in item_affix_pool if a.type != name] - matched_affixes.append(name) - elif any(a.type == name for a in item_affix_pool): - item_affix_pool = [a for a in item_affix_pool if a.type != name] - matched_affixes.append(name) - return matched_affixes - - def _check_non_unique_item(self, item: Item) -> FilterResult: - # TODO replace me next league - res = FilterResult(False, []) - if item.rarity != ItemRarity.Unique and item.type != ItemType.Sigil: - for profile_str, affix_filter in self.affix_filters.items(): - for filter_dict in affix_filter: - for filter_name, filter_data in filter_dict.items(): - filter_min_affix_count = ( - filter_data["minAffixCount"] - if "minAffixCount" in filter_data and filter_data["minAffixCount"] is not None - else 0 - ) - max_power = filter_data["maxPower"] if "maxPower" in filter_data and filter_data["maxPower"] is not None else 9999 - min_power = filter_data["minPower"] if "minPower" in filter_data and filter_data["minPower"] is not None else 1 - power_ok = self._check_item_power(max_power=max_power, min_power=min_power, item_power=item.power) - if "itemType" not in filter_data: - type_ok = True - else: - filter_item_type_list = [ - ItemType(val) - for val in ( - [filter_data["itemType"]] if isinstance(filter_data["itemType"], str) else filter_data["itemType"] - ) - ] - type_ok = self._check_item_type(filter_item_type_list, item.type) - if not power_ok or not type_ok: - continue - matched_affixes = self._match_affixes(filter_data["affixPool"], item.affixes) - affixes_ok = filter_min_affix_count is None or len(matched_affixes) >= filter_min_affix_count - inherent_ok = True - matched_inherent = [] - if "inherentPool" in filter_data: - matched_inherent = self._match_affixes(filter_data["inherentPool"], item.inherent) - inherent_ok = len(matched_inherent) > 0 - if affixes_ok and inherent_ok: - all_matched_affixes = matched_affixes + matched_inherent - affix_debug_msg = [name for name in all_matched_affixes] - Logger.info(f"Matched {profile_str}.Affixes.{filter_name}: {affix_debug_msg}") - res.keep = True - res.matched.append(MatchedFilter(f"{profile_str}.{filter_name}", all_matched_affixes)) - - if item.aspect: - for profile_str, aspect_filter in self.aspect_filters.items(): - for filter_data in aspect_filter: - aspect_name, *rest = filter_data if isinstance(filter_data, list) else [filter_data] - threshold = rest[0] if rest else None - condition = rest[1] if len(rest) > 1 else "larger" - - if item.aspect.type == aspect_name: - if ( - threshold is None - or item.aspect.value is None - or (isinstance(condition, str) and condition == "larger" and item.aspect.value >= threshold) - or (isinstance(condition, str) and condition == "smaller" and item.aspect.value <= threshold) - ): - Logger.info(f"Matched {profile_str}.Aspects: [{item.aspect.type}, {item.aspect.value}]") - res.keep = True - res.matched.append(MatchedFilter(f"{profile_str}.Aspects", did_match_aspect=True)) - return res - - def _check_sigil(self, item: Item) -> FilterResult: - res = FilterResult(False, []) - if ( - len(self.sigil_filters.items()) == 0 - ): # in this intermedia version there are no profiles with Sigils = None since they would have been filtered on load - res.keep = True - res.matched.append(MatchedFilter("")) - for profile_name, profile_filter in self.sigil_filters.items(): - # check item power - if not self._check_item_power(max_power=profile_filter.maxTier, min_power=profile_filter.minTier, item_power=item.power): - continue - # check affix TODO - if profile_filter.blacklist and self._match_affixes(profile_filter["blacklist"], item.affixes + item.inherent): - continue - if profile_filter.whitelist and not self._match_affixes(profile_filter["whitelist"], item.affixes + item.inherent): - continue - Logger.info(f"Matched {profile_name}.Sigils") - res.keep = True - res.matched.append(MatchedFilter(f"{profile_name}")) - return res - - def _check_unique_item(self, item: Item) -> FilterResult: - res = FilterResult(False, []) - for profile_name, profile_filter in self.unique_filters.items(): - for filter_item in profile_filter: - # check item type - if not self._check_item_type(expected_item_types=filter_item.itemType, item_type=item.type): - continue - # check item power - if not self._check_item_power(min_power=filter_item.minPower, item_power=item.power): - continue - # check aspect - if ( - item.aspect is None - or filter_item.aspect is None - or not self._check_item_aspect(expected_aspect=filter_item.aspect, item_aspect=item.aspect) - ): - continue - # check affixes TODO - filter_item.setdefault("affixPool", []) - matched_affixes = self._match_affixes([] if "affixPool" not in filter_item else filter_item["affixPool"], item.affixes) - if len(matched_affixes) != len(filter_item["affixPool"]): - continue - Logger.info(f"Matched {profile_name}.Uniques: {item.aspect.type}") - res.keep = True - res.matched.append(MatchedFilter(f"{profile_name}.{item.aspect.type}", did_match_aspect=True)) - return res - def should_keep(self, item: Item) -> FilterResult: if not self.files_loaded or self._did_files_change(): self.load_files() res = FilterResult(False, []) - if item.type is None or item.power is None: + if item.item_type is None or item.power is None: return res - if item.type == ItemType.Sigil: + if item.item_type == ItemType.Sigil: return self._check_sigil(item) - if item.rarity != ItemRarity.Unique and item.type != ItemType.Sigil: - return self._check_non_unique_item(item) - if item.rarity == ItemRarity.Unique: return self._check_unique_item(item) + if item.rarity != ItemRarity.Unique: + keep_affixes = self._check_affixes(item) + if keep_affixes.keep: + return keep_affixes + if item.rarity == ItemRarity.Legendary: + return self._check_aspect(item) + return res diff --git a/src/item/models.py b/src/item/models.py index 6c2a5853..10ea3058 100644 --- a/src/item/models.py +++ b/src/item/models.py @@ -11,7 +11,7 @@ @dataclass class Item: rarity: ItemRarity | None = None - type: ItemType | None = None + item_type: ItemType | None = None power: int | None = None aspect: Aspect | None = None affixes: list[Affix] = field(default_factory=list) @@ -30,7 +30,7 @@ def __eq__(self, other): if self.power != other.power: Logger.debug("Power not the same") res = False - if self.type != other.type: + if self.item_type != other.item_type: Logger.debug("Type not the same") res = False if self.affixes != other.affixes: @@ -47,7 +47,7 @@ def default(self, o): if isinstance(o, Item): return { "rarity": o.rarity.value if o.rarity else None, - "type": o.type.value if o.type else None, + "item_type": o.item_type.value if o.item_type else None, "power": o.power if o.power else None, "aspect": o.aspect.__dict__ if o.aspect else None, "affixes": [affix.__dict__ for affix in o.affixes], diff --git a/src/loot_filter.py b/src/loot_filter.py index 10a8c720..457e6e35 100644 --- a/src/loot_filter.py +++ b/src/loot_filter.py @@ -1,6 +1,7 @@ import time import keyboard +from PIL import Image # noqa Somehow needed, otherwise the binary has an issue with tesserocr from cam import Cam from config.loader import IniConfigLoader @@ -65,16 +66,16 @@ def check_items(inv: InventoryBase): Logger.debug(f" Runtime (ReadItem): {time.time() - start_time_read:.2f}s") # Hardcoded filters - if rarity == ItemRarity.Common and item_descr.type == ItemType.Material: + if rarity == ItemRarity.Common and item_descr.item_type == ItemType.Material: Logger.info(f"Matched: Material") continue - if rarity == ItemRarity.Legendary and item_descr.type == ItemType.Material: + if rarity == ItemRarity.Legendary and item_descr.item_type == ItemType.Material: Logger.info(f"Matched: Extracted Aspect") continue - elif rarity == ItemRarity.Magic and item_descr.type == ItemType.Elixir: + elif rarity == ItemRarity.Magic and item_descr.item_type == ItemType.Elixir: Logger.info(f"Matched: Elixir") continue - elif rarity in [ItemRarity.Magic, ItemRarity.Common] and item_descr.type != ItemType.Sigil: + elif rarity in [ItemRarity.Magic, ItemRarity.Common] and item_descr.item_type != ItemType.Sigil: keyboard.send("space") wait(0.13, 0.14) continue diff --git a/src/main.py b/src/main.py index 4ee80e12..02ceb101 100644 --- a/src/main.py +++ b/src/main.py @@ -5,14 +5,16 @@ import keyboard from beautifultable import BeautifulTable +from cam import Cam from config.loader import IniConfigLoader from item.filter import Filter from logger import Logger from overlay import Overlay from utils.game_settings import is_fontsize_ok +from utils.misc import wait from utils.ocr.read import load_api from utils.process_handler import safe_exit -from utils.window import WindowSpec +from utils.window import start_detecting_window, WindowSpec from version import __version__ @@ -26,6 +28,9 @@ def main(): Logger.init("info") win_spec = WindowSpec(IniConfigLoader().advanced_options.process_name) + start_detecting_window(win_spec) + while not Cam().is_offset_set(): + wait(0.2) load_api() diff --git a/src/scripts/vision_mode.py b/src/scripts/vision_mode.py index 59beffed..d3814a7a 100644 --- a/src/scripts/vision_mode.py +++ b/src/scripts/vision_mode.py @@ -165,7 +165,6 @@ def vision_mode(): # Check if the item is a match based on our filters match = True - item_descr = None last_top_left_corner = top_left_corner last_center = item_center item_descr = read_descr(rarity, cropped_descr, False) @@ -175,16 +174,16 @@ def vision_mode(): continue ignored_item = False - if rarity == ItemRarity.Common and item_descr.type == ItemType.Material: + if rarity == ItemRarity.Common and item_descr.item_type == ItemType.Material: Logger.info(f"Matched: Material") ignored_item = True - elif rarity == ItemRarity.Legendary and item_descr.type == ItemType.Material: + elif rarity == ItemRarity.Legendary and item_descr.item_type == ItemType.Material: Logger.info(f"Matched: Extracted Aspect") ignored_item = True - elif rarity == ItemRarity.Magic and item_descr.type == ItemType.Elixir: + elif rarity == ItemRarity.Magic and item_descr.item_type == ItemType.Elixir: Logger.info(f"Matched: Elixir") ignored_item = True - elif rarity in [ItemRarity.Magic, ItemRarity.Common] and item_descr.type != ItemType.Sigil: + elif rarity in [ItemRarity.Magic, ItemRarity.Common] and item_descr.item_type != ItemType.Sigil: match = False if ignored_item: diff --git a/src/template_finder.py b/src/template_finder.py index b16c2caf..72948d13 100644 --- a/src/template_finder.py +++ b/src/template_finder.py @@ -73,7 +73,7 @@ def detect(self, img: np.ndarray = None) -> SearchResult: if img is not None: self.inp_img = img else: - img = Cam().grab() if self.inp_img is None else self.inp_img + Cam().grab() if self.inp_img is None else self.inp_img return search(**self.as_dict()) def is_visible(self, img: np.ndarray = None) -> bool: @@ -303,7 +303,7 @@ def _process_cv_result(template: Template, img: np.ndarray) -> bool: if len(matches) > 1 and mode == "all": Logger.debug( "Found the following matches:\n" - + ", ".join([" {template_match.name} ({template_match.score*100:.1f}% confidence)" for template_match in matches]) + + ", ".join([" {template_match.name} ({template_match.score*100:.1f}% confidence)" for _ in matches]) ) else: Logger.debug("Found {mode} match: {template_match.name} ({template_match.score*100:.1f}% confidence)") diff --git a/src/utils/custom_mouse.py b/src/utils/custom_mouse.py index 87987a9e..c9d3aebc 100644 --- a/src/utils/custom_mouse.py +++ b/src/utils/custom_mouse.py @@ -1,12 +1,12 @@ # Mostly copied from: https://github.com/patrikoss/pyclick -import mouse as _mouse -from mouse import _winmouse -import pytweening -import numpy as np -import random import math +import random import time -from cam import Cam + +import mouse as _mouse +import numpy as np +import pytweening +from mouse import _winmouse def isNumeric(val): @@ -19,7 +19,7 @@ def isListOfPoints(l): try: isPoint = lambda p: ((len(p) == 2) and isNumeric(p[0]) and isNumeric(p[1])) return all(map(isPoint, l)) - except (KeyError, TypeError) as e: + except (KeyError, TypeError): return False diff --git a/src/utils/window.py b/src/utils/window.py index e300de2a..49fc412b 100644 --- a/src/utils/window.py +++ b/src/utils/window.py @@ -61,7 +61,7 @@ def _get_process_from_window_name(hwnd: int) -> str: try: pid = GetWindowThreadProcessId(hwnd)[1] return psutil.Process(pid).name().lower() - except Exception as e: + except Exception: return "" diff --git a/test/config/data/sigils.py b/test/config/data/sigils.py index eb32a85d..df029035 100644 --- a/test/config/data/sigils.py +++ b/test/config/data/sigils.py @@ -1,32 +1,32 @@ all_bad_cases = [ # 1 item - {"Sigil": {"blacklist": "monster_cold_resist"}}, - {"Sigil": {"blacklist": "monster_cold_resist"}}, - {"Sigil": {"blacklist": ["monster123_cold_resist"]}}, - {"Sigil": {"blacklist": ["monster_cold_resist", "test123"]}}, - {"Sigil": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_cold_resist"]}}, - {"Sigil": {"maxTier": 101}}, - {"Sigil": {"minTier": -1}}, - {"Sigil": {"whitelist": ["monster123_cold_resist"]}}, - {"Sigil": {"whitelist": ["monster_cold_resist", "test123"]}}, + {"Sigils": {"blacklist": "monster_cold_resist"}}, + {"Sigils": {"blacklist": "monster_cold_resist"}}, + {"Sigils": {"blacklist": ["monster123_cold_resist"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist", "test123"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_cold_resist"]}}, + {"Sigils": {"maxTier": 101}}, + {"Sigils": {"minTier": -1}}, + {"Sigils": {"whitelist": ["monster123_cold_resist"]}}, + {"Sigils": {"whitelist": ["monster_cold_resist", "test123"]}}, ] all_good_cases = [ # 1 item - {"Sigil": {"blacklist": ["monster_cold_resist"]}}, - {"Sigil": {"maxTier": 90}}, - {"Sigil": {"minTier": 10}}, - {"Sigil": {"whitelist": ["monster_cold_resist"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist"]}}, + {"Sigils": {"maxTier": 90}}, + {"Sigils": {"minTier": 10}}, + {"Sigils": {"whitelist": ["monster_cold_resist"]}}, # 2 items - {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90}}, - {"Sigil": {"blacklist": ["monster_cold_resist"], "minTier": 10}}, - {"Sigil": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_fire_resist"]}}, - {"Sigil": {"maxTier": 90, "minTier": 10}}, - {"Sigil": {"maxTier": 90, "whitelist": ["monster_cold_resist"]}}, - {"Sigil": {"minTier": 10, "whitelist": ["monster_cold_resist"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "maxTier": 90}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "minTier": 10}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "whitelist": ["monster_fire_resist"]}}, + {"Sigils": {"maxTier": 90, "minTier": 10}}, + {"Sigils": {"maxTier": 90, "whitelist": ["monster_cold_resist"]}}, + {"Sigils": {"minTier": 10, "whitelist": ["monster_cold_resist"]}}, # 3 items - {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "minTier": 10}}, - {"Sigil": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "whitelist": ["monster_fire_resist"]}}, - {"Sigil": {"blacklist": ["monster_cold_resist"], "minTier": 10, "whitelist": ["monster_fire_resist"]}}, - {"Sigil": {"maxTier": 90, "minTier": 10, "whitelist": ["monster_cold_resist"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "minTier": 10}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "maxTier": 90, "whitelist": ["monster_fire_resist"]}}, + {"Sigils": {"blacklist": ["monster_cold_resist"], "minTier": 10, "whitelist": ["monster_fire_resist"]}}, + {"Sigils": {"maxTier": 90, "minTier": 10, "whitelist": ["monster_cold_resist"]}}, ] diff --git a/test/item/filter/data/affixes.py b/test/item/filter/data/affixes.py index a8f3fc67..aa8ee40c 100644 --- a/test/item/filter/data/affixes.py +++ b/test/item/filter/data/affixes.py @@ -10,13 +10,13 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): affixes = [ - ("wrong type", [], TestItem(type=ItemType.Amulet)), - ("power too low", [], TestItem(type=ItemType.Helm, power=724)), + ("wrong type", [], TestItem(item_type=ItemType.Amulet)), + ("power too low", [], TestItem(item_type=ItemType.Helm, power=724)), ( "res boots 4 res", [], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="cold_resistance", value=5), Affix(type="fire_resistance", value=5), @@ -29,7 +29,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "res boots 3 res", [], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="cold_resistance", value=5), Affix(type="fire_resistance", value=5), @@ -41,7 +41,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "res boots 3 res+ms", ["test.ResBoots"], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="cold_resistance", value=5), Affix(type="movement_speed", value=5), @@ -54,7 +54,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "res boots 2 res", [], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="cold_resistance", value=5), Affix(type="shadow_resistance", value=5), @@ -65,7 +65,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "res boots 2 res+ms", ["test.ResBoots"], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="cold_resistance", value=5), Affix(type="movement_speed", value=5), @@ -77,7 +77,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "helm life", [], TestItem( - type=ItemType.Helm, + item_type=ItemType.Helm, affixes=[ Affix(type="maximum_life", value=5), Affix(type="movement_speed", value=5), @@ -86,24 +86,11 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): ], ), ), - ( - "helm no life", - ["test.HelmNoLife"], - TestItem( - type=ItemType.Helm, - affixes=[ - Affix(type="cold_resistance", value=5), - Affix(type="movement_speed", value=5), - Affix(type="fire_resistance", value=5), - Affix(type="shadow_resistance", value=5), - ], - ), - ), ( "boots inherent", ["test.GreatBoots", "test.ResBoots"], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="movement_speed", value=5), Affix(type="cold_resistance", value=5), @@ -116,7 +103,7 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): "boots no inherent", ["test.ResBoots"], TestItem( - type=ItemType.Boots, + item_type=ItemType.Boots, affixes=[ Affix(type="movement_speed", value=5), Affix(type="cold_resistance", value=5), @@ -126,6 +113,3 @@ def __init__(self, rarity=ItemRarity.Rare, power=910, **kwargs): ), ), ] - -# wrong everything -# with affix diff --git a/test/item/filter/data/aspects.py b/test/item/filter/data/aspects.py index 71a3c5a0..876fd503 100644 --- a/test/item/filter/data/aspects.py +++ b/test/item/filter/data/aspects.py @@ -5,8 +5,8 @@ class TestLegendary(Item): - def __init__(self, rarity=ItemRarity.Legendary, type=ItemType.Shield, power=910, **kwargs): - super().__init__(rarity=rarity, type=type, power=power, **kwargs) + def __init__(self, rarity=ItemRarity.Legendary, item_type=ItemType.Shield, power=910, **kwargs): + super().__init__(rarity=rarity, item_type=item_type, power=power, **kwargs) aspects = [ diff --git a/test/item/filter/data/filters.py b/test/item/filter/data/filters.py index 4cd327ac..d8684bae 100644 --- a/test/item/filter/data/filters.py +++ b/test/item/filter/data/filters.py @@ -1,85 +1,125 @@ -from config.models import UniqueModel, AffixAspectFilterModel, ComparisonType, ProfileModel, SigilModel +from config.models import ( + UniqueModel, + ComparisonType, + ProfileModel, + SigilModel, + AffixFilterModel, + AspectUniqueFilterModel, + AspectFilterModel, + AffixFilterCountModel, + ItemFilterModel, +) from item.data.item_type import ItemType -affix = { - "test": [ +# noinspection PyTypeChecker +affix = ProfileModel( + name="test", + Affixes=[ { - "Helm": { - "itemType": "helm", - "minPower": 725, - "affixPool": [["basic_skill_attack_speed", 6], ["cooldown_reduction", 5], ["maximum_life", 640], ["total_armor", 9]], - } + "Helm": ItemFilterModel( + itemType=[ItemType.Helm], + minPower=725, + affixPool=[ + AffixFilterCountModel( + count=[ + AffixFilterModel(name="basic_skill_attack_speed", value=6), + AffixFilterModel(name="cooldown_reduction", value=5), + AffixFilterModel(name="maximum_life", value=640), + AffixFilterModel(name="total_armor", value=9), + ] + ) + ], + ) }, { - "ResBoots": { - "itemType": "boots", - "minPower": 725, - "affixPool": [ - ["movement_speed"], - { - "any_of": [ - ["shadow_resistance"], - ["cold_resistance"], - ["lightning_resistance"], - ["poison_resistance"], - ["fire_resistance"], + "ResBoots": ItemFilterModel( + itemType=[ItemType.Boots], + minPower=725, + affixPool=[ + AffixFilterCountModel(count=[AffixFilterModel(name="movement_speed")]), + AffixFilterCountModel( + count=[ + AffixFilterModel(name="shadow_resistance"), + AffixFilterModel(name="cold_resistance"), + AffixFilterModel(name="lightning_resistance"), + AffixFilterModel(name="poison_resistance"), + AffixFilterModel(name="fire_resistance"), ], - "minCount": 2, - }, + minCount=2, + ), ], - } - }, - { - "GreatBoots": { - "affixPool": [["movement_speed"], ["cold_resistance"], ["lightning_resistance"]], - "inherentPool": ["maximum_evade_charges", "attacks_reduce_evades_cooldown"], - "itemType": "boots", - "minPower": 800, - } + ) }, { - "HelmNoLife": { - "itemType": "helm", - "minPower": 725, - "affixPool": [ - {"blacklist": ["maximum_life"]}, + "GreatBoots": ItemFilterModel( + itemType=[ItemType.Boots], + minPower=725, + affixPool=[ + AffixFilterCountModel( + count=[ + AffixFilterModel(name="movement_speed"), + AffixFilterModel(name="cold_resistance"), + AffixFilterModel(name="lightning_resistance"), + ], + ), + ], + inherentPool=[ + AffixFilterCountModel( + count=[ + AffixFilterModel(name="maximum_evade_charges"), + AffixFilterModel(name="attacks_reduce_evades_cooldown_by_seconds"), + ], + minCount=1, + ), ], - } + ) }, { - "Armor": { - "itemType": ["chest armor", "pants"], - "minPower": 725, - "affixPool": [ - ["damage_reduction_from_close_enemies", 10], - ["maximum_life", 700], - ["dodge_chance_against_close_enemies", 6.5], - ["dodge_chance", 5.0], + "Armor": ItemFilterModel( + itemType=[ItemType.ChestArmor, ItemType.Legs], + minPower=725, + affixPool=[ + AffixFilterCountModel( + count=[ + AffixFilterModel(name="damage_reduction_from_close_enemies", value=10), + AffixFilterModel(name="maximum_life", value=700), + AffixFilterModel(name="dodge_chance_against_close_enemies", value=6.5), + AffixFilterModel(name="dodge_chance", value=5), + ], + ), ], - } + ) }, { - "Boots": { - "itemType": "boots", - "minPower": 725, - "affixPool": [ - ["movement_speed", 10], - ["maximum_life", 700], - ["cold_resistance", 6.5], - ["fire_resistance", 5.0], - ["poison_resistance", 5.0], - ["shadow_resistance", 5.0], + "Boots": ItemFilterModel( + itemType=[ItemType.Boots], + minPower=725, + affixPool=[ + AffixFilterCountModel( + count=[ + AffixFilterModel(name="movement_speed", value=10), + AffixFilterModel(name="maximum_life", value=700), + AffixFilterModel(name="cold_resistance", value=6.5), + AffixFilterModel(name="fire_resistance", value=5), + AffixFilterModel(name="poison_resistance", value=5), + AffixFilterModel(name="shadow_resistance", value=5), + ], + minCount=4, + ), ], - } + ) }, - ] -} -aspect = { - "test": [ - ["accelerating", 25], - ["of_might", 6.0, "smaller"], - ] -} + ], +) + +aspect = ProfileModel( + name="test", + Aspects=[ + AspectFilterModel(name="accelerating", value=25), + AspectFilterModel(name="of_might", value=6.0, comparison=ComparisonType.smaller), + ], +) + sigil = ProfileModel( name="test", Sigils=SigilModel(blacklist=["reduce_cooldowns_on_kill", "vault_of_copper"], whitelist=["jalals_vigil"], maxTier=80, minTier=40), @@ -92,19 +132,19 @@ UniqueModel(itemType=[ItemType.Scythe], minPower=900), UniqueModel( affix=[ - AffixAspectFilterModel(name="attack_speed", value=8.4), - AffixAspectFilterModel(name="lucky_hit_up_to_a_chance_to_restore_primary_resource", value=12), - AffixAspectFilterModel(name="maximum_life", value=700), - AffixAspectFilterModel(name="maximum_essence", value=9), + AffixFilterModel(name="attack_speed", value=8.4), + AffixFilterModel(name="lucky_hit_up_to_a_chance_to_restore_primary_resource", value=12), + AffixFilterModel(name="maximum_life", value=700), + AffixFilterModel(name="maximum_essence", value=9), ], - aspect=AffixAspectFilterModel(name="lidless_wall", value=20), + aspect=AspectUniqueFilterModel(name="lidless_wall", value=20), minPower=900, ), UniqueModel( - affix=[AffixAspectFilterModel(name="attack_speed", value=8.4)], - aspect=AffixAspectFilterModel(name="soulbrand", value=20), + affix=[AffixFilterModel(name="attack_speed", value=8.4)], + aspect=AspectUniqueFilterModel(name="soulbrand", value=20), minPower=900, ), - UniqueModel(aspect=AffixAspectFilterModel(name="soulbrand", value=15, comparison=ComparisonType.smaller)), + UniqueModel(aspect=AspectUniqueFilterModel(name="soulbrand", value=15, comparison=ComparisonType.smaller), minPower=900), ], ) diff --git a/test/item/filter/data/sigils.py b/test/item/filter/data/sigils.py index de85aa25..3d93dc2f 100644 --- a/test/item/filter/data/sigils.py +++ b/test/item/filter/data/sigils.py @@ -5,8 +5,8 @@ class TestSigil(Item): - def __init__(self, rarity=ItemRarity.Common, type=ItemType.Sigil, power=60, **kwargs): - super().__init__(rarity=rarity, type=type, power=power, **kwargs) + def __init__(self, rarity=ItemRarity.Common, item_type=ItemType.Sigil, power=60, **kwargs): + super().__init__(rarity=rarity, item_type=item_type, power=power, **kwargs) sigils = [ diff --git a/test/item/filter/data/uniques.py b/test/item/filter/data/uniques.py index 701faf7e..89e4b0ec 100644 --- a/test/item/filter/data/uniques.py +++ b/test/item/filter/data/uniques.py @@ -6,20 +6,20 @@ class TestUnique(Item): - def __init__(self, rarity=ItemRarity.Unique, type=ItemType.Shield, power=910, **kwargs): - super().__init__(rarity=rarity, type=type, power=power, **kwargs) + def __init__(self, rarity=ItemRarity.Unique, item_type=ItemType.Shield, power=910, **kwargs): + super().__init__(rarity=rarity, item_type=item_type, power=power, **kwargs) uniques = [ ( - "power too low", + "item power too low", [], TestUnique(power=800), ), ( "wrong type", [], - TestUnique(type=ItemType.Helm, aspect=Aspect(type="deathless_visage", value=1862)), + TestUnique(item_type=ItemType.Helm, aspect=Aspect(type="deathless_visage", value=1862)), ), ( "wrong aspect", @@ -79,7 +79,7 @@ def __init__(self, rarity=ItemRarity.Unique, type=ItemType.Shield, power=910, ** "ok_2", ["test.black_river", "test.black_river"], TestUnique( - type=ItemType.Scythe, + item_type=ItemType.Scythe, aspect=Aspect(type="black_river", value=128), ), ), diff --git a/test/item/filter/filter_test.py b/test/item/filter/filter_test.py index 3834ee20..df135126 100644 --- a/test/item/filter/filter_test.py +++ b/test/item/filter/filter_test.py @@ -3,7 +3,7 @@ from pytest_mock import MockerFixture import test.item.filter.data.filters as filters -from item.filter import Filter +from item.filter import Filter, FilterResult from item.models import Item from test.item.filter.data.affixes import affixes from test.item.filter.data.aspects import aspects @@ -19,18 +19,18 @@ def _create_mocked_filter(mocker: MockerFixture) -> Filter: @pytest.mark.parametrize("name, result, item", natsorted(affixes), ids=[name for name, _, _ in natsorted(affixes)]) -@pytest.mark.skip(reason="for later") def test_affixes(name: str, result: list[str], item: Item, mocker: MockerFixture): test_filter = _create_mocked_filter(mocker) - test_filter.affix_filters = filters.affix + mocker.patch("item.filter.Filter._check_aspect", return_value=FilterResult(keep=False, matched=[])) + test_filter.affix_filters = {filters.affix.name: filters.affix.Affixes} assert natsorted([match.profile for match in test_filter.should_keep(item).matched]) == natsorted(result) @pytest.mark.parametrize("name, result, item", natsorted(aspects), ids=[name for name, _, _ in natsorted(aspects)]) -@pytest.mark.skip(reason="for later") def test_aspects(name: str, result: list[str], item: Item, mocker: MockerFixture): test_filter = _create_mocked_filter(mocker) - test_filter.aspect_filters = filters.aspect + mocker.patch("item.filter.Filter._check_affixes", return_value=FilterResult(keep=False, matched=[])) + test_filter.aspect_filters = {filters.aspect.name: filters.aspect.Aspects} assert natsorted([match.profile.split(".")[0] for match in test_filter.should_keep(item).matched]) == natsorted(result) From a3af5282b9e9c5cf3747b140d609562b2e4674a6 Mon Sep 17 00:00:00 2001 From: chrisoro Date: Sat, 20 Apr 2024 21:09:33 +0200 Subject: [PATCH 3/3] oops --- src/loot_filter.py | 1 - src/main.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/loot_filter.py b/src/loot_filter.py index 457e6e35..e10475ac 100644 --- a/src/loot_filter.py +++ b/src/loot_filter.py @@ -1,7 +1,6 @@ import time import keyboard -from PIL import Image # noqa Somehow needed, otherwise the binary has an issue with tesserocr from cam import Cam from config.loader import IniConfigLoader diff --git a/src/main.py b/src/main.py index 02ceb101..01d40d46 100644 --- a/src/main.py +++ b/src/main.py @@ -3,6 +3,7 @@ from pathlib import Path import keyboard +from PIL import Image # noqa Somehow needed, otherwise the binary has an issue with tesserocr from beautifultable import BeautifulTable from cam import Cam