-
Notifications
You must be signed in to change notification settings - Fork 43
seals and charms #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
seals and charms #763
Changes from 15 commits
6b1e178
04893ce
2a0b7cf
673d6dd
afbbc31
9ac225c
2e4b556
468a9f4
d63e90f
86a4de5
7eefefc
6fc38b2
cf3fec5
f4269be
79c01c9
833fd27
1e29bc9
e1caf1b
7061492
78a9491
f1596d8
29fccd0
615cd4e
e7a0067
d4ac0f8
8e5c298
cee048d
5ca8ff2
67677ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,9 +6,10 @@ | |
|
|
||
| from pydantic import BaseModel, ConfigDict, Field, RootModel, field_validator, model_validator | ||
|
|
||
| from src.config.helper import check_greater_than_zero, validate_percent | ||
| from src.config.helper import check_greater_than_zero, validate_greater_affix_count, validate_percent | ||
| from src.item.data.item_type import ItemType # noqa: TC001 | ||
| from src.item.data.rarity import ItemRarity | ||
| from src.scripts import correct_name | ||
|
|
||
| MODULE_LOGGER = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -19,6 +20,36 @@ def _parse_item_type_or_rarities(data: str | list[str]) -> list[str]: | |
| return data | ||
|
|
||
|
|
||
| def _coerce_name_rarity_filter_data(data: str | list[str] | dict[str, str | list[str]]) -> dict[str, str | list[str]]: | ||
| if isinstance(data, dict): | ||
| return data | ||
| if isinstance(data, str): | ||
| if any(rarity.value.lower() == data.lower() for rarity in ItemRarity): | ||
| return {"rarities": [data]} | ||
| return {"name": data} | ||
| if isinstance(data, list): | ||
| if not data: | ||
| msg = "list cannot be empty" | ||
| raise ValueError(msg) | ||
| return {"rarities": data} | ||
| msg = "must be str or list" | ||
| raise ValueError(msg) | ||
|
|
||
|
|
||
| def _normalize_existing_set_name(name: str | None, field_name: str) -> str | None: | ||
| if not name: | ||
| return None | ||
|
|
||
| # This on module level would be a circular import, so we do it lazy for now | ||
| from src.dataloader import Dataloader # noqa: PLC0415 | ||
|
|
||
| name = correct_name(name) | ||
| if name not in Dataloader().set_list: | ||
| msg = f"{field_name} {name} does not exist" | ||
| raise ValueError(msg) | ||
| return name | ||
|
|
||
|
|
||
| class AffixAspectFilterModel(BaseModel): | ||
| model_config = ConfigDict(extra="forbid") | ||
| name: str | ||
|
|
@@ -149,10 +180,7 @@ def check_min_power(cls, v: int) -> int: | |
| @field_validator("min_greater_affix_count") | ||
| @classmethod | ||
| def count_validator(cls, v: int) -> int: | ||
| if not 0 <= v <= 4: | ||
| msg = "must be in [0, 4]" | ||
| raise ValueError(msg) | ||
| return v | ||
| return validate_greater_affix_count(v) | ||
|
|
||
| @field_validator("min_percent_of_aspect") | ||
| @classmethod | ||
|
|
@@ -177,10 +205,7 @@ def check_min_power(cls, v: int) -> int: | |
| @field_validator("min_greater_affix_count") | ||
| @classmethod | ||
| def min_greater_affix_in_range(cls, v: int) -> int: | ||
| if not 0 <= v <= 4: | ||
| msg = "must be in [0, 4]" | ||
| raise ValueError(msg) | ||
| return v | ||
| return validate_greater_affix_count(v) | ||
|
|
||
| @field_validator("item_type", mode="before") | ||
| @classmethod | ||
|
|
@@ -207,6 +232,91 @@ def unique_aspect_names_must_be_unique(self) -> ItemFilterModel: | |
| DynamicItemFilterModel = RootModel[dict[str, ItemFilterModel]] | ||
|
|
||
|
|
||
| class SealCharmFilterModel(BaseModel): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make sure we keep everything alphabetical, this should be lower in the file |
||
| model_config = ConfigDict(extra="forbid", populate_by_name=True) | ||
| affix_pool: list[AffixFilterCountModel] = Field(default=[], alias="affixPool") | ||
| min_greater_affix_count: int = Field(default=0, alias="minGreaterAffixCount") | ||
| rarities: list[ItemRarity] = [] | ||
|
|
||
| @field_validator("min_greater_affix_count") | ||
| @classmethod | ||
| def min_greater_affix_in_range(cls, v: int) -> int: | ||
| return validate_greater_affix_count(v) | ||
|
|
||
| @field_validator("rarities", mode="before") | ||
| @classmethod | ||
| def parse_rarities(cls, data: str | list[str]) -> list[str]: | ||
| return _parse_item_type_or_rarities(data) | ||
|
|
||
|
|
||
| class CharmFilterModel(SealCharmFilterModel): | ||
| set_name: str | None = Field(default=None, alias="set") | ||
| unique_aspect: str | None = Field(default=None, alias="uniqueAspect") | ||
|
|
||
| @field_validator("set_name") | ||
| @classmethod | ||
| def set_must_exist(cls, name: str | None) -> str | None: | ||
| return _normalize_existing_set_name(name, "set") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
| @field_validator("unique_aspect") | ||
| @classmethod | ||
| def normalize_unique_aspect(cls, name: str | None) -> str | None: | ||
| return correct_name(name) | ||
|
cjshrader marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class BoostedSetFilterModel(BaseModel): | ||
| model_config = ConfigDict(extra="forbid", populate_by_name=True) | ||
| affix: AffixFilterModel | None = None | ||
| required: bool = False | ||
| set_name: str = Field(alias="set") | ||
|
|
||
| @field_validator("set_name") | ||
| @classmethod | ||
| def set_must_exist(cls, name: str) -> str: | ||
| normalized_name = _normalize_existing_set_name(name, "set") | ||
| if normalized_name is None: | ||
| msg = "set must not be empty" | ||
| raise ValueError(msg) | ||
| return normalized_name | ||
|
|
||
| @model_validator(mode="after") | ||
| def required_boosted_affix_must_have_affix(self) -> BoostedSetFilterModel: | ||
| if self.required and self.affix is None: | ||
| msg = "required boostedSets entries need affix" | ||
|
cjshrader marked this conversation as resolved.
|
||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
|
|
||
| class SealFilterModel(SealCharmFilterModel): | ||
| boosted_affix: AffixFilterModel | None = Field(default=None, alias="boostedAffix") | ||
|
cjshrader marked this conversation as resolved.
Outdated
|
||
| boosted_affix_required: bool = Field(default=False, alias="boostedAffixRequired") | ||
| boosted_set: str | None = Field(default=None, alias="boostedSet") | ||
| boosted_sets: list[BoostedSetFilterModel] = Field(default=[], alias="boostedSets") | ||
| charm_slots: int = Field(default=0, alias="charmSlots") | ||
|
cjshrader marked this conversation as resolved.
Outdated
|
||
|
|
||
| @field_validator("boosted_set") | ||
| @classmethod | ||
| def boosted_set_must_exist(cls, name: str | None) -> str | None: | ||
| return _normalize_existing_set_name(name, "boostedSet") | ||
|
|
||
| @field_validator("charm_slots") | ||
| @classmethod | ||
| def charm_slots_must_be_positive(cls, v: int) -> int: | ||
| return check_greater_than_zero(v) | ||
|
|
||
| @model_validator(mode="after") | ||
| def required_boosted_affix_must_have_affix(self) -> SealFilterModel: | ||
| if self.boosted_affix_required and (self.boosted_affix is None or self.boosted_set is None): | ||
| msg = "boostedAffixRequired needs boostedSet and boostedAffix" | ||
| raise ValueError(msg) | ||
| return self | ||
|
|
||
|
|
||
| DynamicSealCharmFilterModel = RootModel[dict[str, SealCharmFilterModel]] | ||
|
cjshrader marked this conversation as resolved.
|
||
| DynamicCharmFilterModel = RootModel[dict[str, CharmFilterModel]] | ||
| DynamicSealFilterModel = RootModel[dict[str, SealFilterModel]] | ||
|
|
||
|
|
||
| class SigilPriority(enum.StrEnum): | ||
| blacklist = enum.auto() | ||
| whitelist = enum.auto() | ||
|
|
@@ -295,19 +405,28 @@ def name_must_exist(cls, name: str) -> str: | |
| @model_validator(mode="before") | ||
| @classmethod | ||
| def parse_data(cls, data: str | list[str] | dict[str, str | list[str]]) -> dict[str, str | list[str]]: | ||
| if isinstance(data, dict): | ||
| return data | ||
| if isinstance(data, str): | ||
| if any(rarity.value.lower() == data.lower() for rarity in ItemRarity): | ||
| return {"rarities": [data]} | ||
| return {"name": data} | ||
| if isinstance(data, list): | ||
| if not data: | ||
| msg = "list cannot be empty" | ||
| raise ValueError(msg) | ||
| return {"rarities": data} | ||
| msg = "must be str or list" | ||
| raise ValueError(msg) | ||
| return _coerce_name_rarity_filter_data(data) | ||
|
|
||
| @field_validator("rarities", mode="before") | ||
| @classmethod | ||
| def parse_rarities(cls, data: str | list[str]) -> list[str]: | ||
| return _parse_item_type_or_rarities(data) | ||
|
|
||
|
|
||
| class NameRarityFilterModel(BaseModel): | ||
|
cjshrader marked this conversation as resolved.
Outdated
|
||
| model_config = ConfigDict(extra="forbid") | ||
| name: str | None = None | ||
| rarities: list[ItemRarity] = [] | ||
|
|
||
| @field_validator("name") | ||
| @classmethod | ||
| def normalize_name(cls, name: str | None) -> str | None: | ||
| return correct_name(name) | ||
|
|
||
| @model_validator(mode="before") | ||
| @classmethod | ||
| def parse_data(cls, data: str | list[str] | dict[str, str | list[str]]) -> dict[str, str | list[str]]: | ||
| return _coerce_name_rarity_filter_data(data) | ||
|
|
||
| @field_validator("rarities", mode="before") | ||
| @classmethod | ||
|
|
@@ -319,8 +438,10 @@ class ProfileModel(BaseModel): | |
| model_config = ConfigDict(extra="forbid", populate_by_name=True) | ||
| affixes: list[DynamicItemFilterModel] = Field(default=[], alias="Affixes") | ||
| aspect_upgrades: list[str] = Field(default=[], alias="AspectUpgrades") | ||
| charms: list[DynamicCharmFilterModel] = Field(default=[], alias="Charms") | ||
| global_uniques: list[GlobalUniqueModel] = Field(default=[], alias="GlobalUniques") | ||
| name: str | ||
| seals: list[DynamicSealFilterModel] = Field(default=[], alias="Seals") | ||
| sigils: SigilFilterModel = Field( | ||
| default=SigilFilterModel(blacklist=[], whitelist=[], priority=SigilPriority.blacklist), alias="Sigils" | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,14 +13,17 @@ | |
| AffixFilterCountModel, | ||
| AffixFilterModel, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wish you had saved this for a future PR, but I guess we'll leave it here now
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For now though I'm not going to be looking at importer code very closely, we'll come back to it after we know filtering works. |
||
| AspectUniqueFilterModel, | ||
| CharmFilterModel, | ||
| ItemFilterModel, | ||
| ProfileModel, | ||
| SealFilterModel, | ||
| ) | ||
| from src.dataloader import Dataloader | ||
| from src.gui.importer.gui_common import ( | ||
| add_mythics_to_filters, | ||
| add_to_profiles, | ||
| build_default_profile_file_name, | ||
| create_seal_charm_filter, | ||
| fix_offhand_type, | ||
| fix_weapon_type, | ||
| get_class_name, | ||
|
|
@@ -95,6 +98,8 @@ def import_d4builds(config: ImportConfig, driver: ChromiumDriver = None): | |
| raise D4BuildsError(msg) | ||
| slot_to_unique_name_map = _get_item_slots(data=data) | ||
| finished_filters = [] | ||
| charm_filters = [] | ||
| seal_filters = [] | ||
| mythic_names = [] | ||
| aspect_upgrade_filters = _get_legendary_aspects(data=data) | ||
| for item in items[0]: | ||
|
|
@@ -177,6 +182,20 @@ def import_d4builds(config: ImportConfig, driver: ChromiumDriver = None): | |
| else: | ||
| item_filter.item_type = [item_type] | ||
|
|
||
| if item_type in [ItemType.HoradricSeal, ItemType.Charm]: | ||
| seal_charm_filters = charm_filters if item_type == ItemType.Charm else seal_filters | ||
| filter_name = _unique_filter_name(item_type.name, seal_charm_filters) | ||
| seal_charm_model = CharmFilterModel if item_type == ItemType.Charm else SealFilterModel | ||
| seal_charm_filters.append({ | ||
| filter_name: create_seal_charm_filter( | ||
| affixes=affixes, | ||
| rarity=rarity, | ||
| require_gas=config.require_greater_affixes, | ||
| model_type=seal_charm_model, | ||
| ) | ||
| }) | ||
| continue | ||
|
|
||
| # We don't bother importing affixes for mythics | ||
| if rarity != ItemRarity.Mythic: | ||
| item_filter.affix_pool = [ | ||
|
|
@@ -192,15 +211,16 @@ def import_d4builds(config: ImportConfig, driver: ChromiumDriver = None): | |
| ] | ||
| item_filter.min_power = 100 | ||
| filter_name_template = item_filter.item_type[0].name if item_type else slot.replace(" ", "") | ||
| filter_name = filter_name_template | ||
| i = 2 | ||
| while any(filter_name == next(iter(x)) for x in finished_filters): | ||
| filter_name = f"{filter_name_template}{i}" | ||
| i += 1 | ||
| filter_name = _unique_filter_name(filter_name_template, finished_filters) | ||
| finished_filters.append({filter_name: item_filter}) | ||
| # Place all mythics in a single filter | ||
| add_mythics_to_filters(mythic_names, finished_filters) | ||
| profile = ProfileModel(name="imported profile", Affixes=sort_profile_filters(finished_filters)) | ||
| profile = ProfileModel( | ||
| name="imported profile", | ||
| Affixes=sort_profile_filters(finished_filters), | ||
| Charms=sort_profile_filters(charm_filters), | ||
| Seals=sort_profile_filters(seal_filters), | ||
| ) | ||
| if config.import_aspect_upgrades and aspect_upgrade_filters: | ||
| profile.aspect_upgrades = aspect_upgrade_filters | ||
|
|
||
|
|
@@ -324,6 +344,15 @@ def _get_affix_name(stat: lxml.html.HtmlElement) -> str: | |
| return "" | ||
|
|
||
|
|
||
| def _unique_filter_name(filter_name_template: str, filters: list[dict]) -> str: | ||
| filter_name = filter_name_template | ||
| i = 2 | ||
| while any(filter_name == next(iter(existing_filter)) for existing_filter in filters): | ||
| filter_name = f"{filter_name_template}{i}" | ||
| i += 1 | ||
| return filter_name | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| src.logger.setup() | ||
| URLS = ["https://d4builds.gg/builds/whirlwind-barbarian-endgame/?var=4"] | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.