Skip to content
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
6b1e178
update1
chrizzocb May 29, 2026
04893ce
update2
chrizzocb May 29, 2026
2a0b7cf
update3
chrizzocb May 30, 2026
673d6dd
update4
chrizzocb May 30, 2026
afbbc31
update5
chrizzocb May 30, 2026
9ac225c
Merge branch 'main' into Update-profile_models
cjshrader May 30, 2026
2e4b556
update6
chrizzocb May 31, 2026
468a9f4
Merge branch 'd4lfteam:main' into Update-profile_models
chrizzocb May 31, 2026
d63e90f
update7
chrizzocb May 31, 2026
86a4de5
update7
chrizzocb May 31, 2026
7eefefc
update9
chrizzocb May 31, 2026
6fc38b2
Merge branch 'd4lfteam:main' into Update-profile_models
chrizzocb Jun 1, 2026
cf3fec5
update10
chrizzocb Jun 2, 2026
f4269be
update11
chrizzocb Jun 2, 2026
79c01c9
update12
chrizzocb Jun 2, 2026
833fd27
update13
chrizzocb Jun 3, 2026
1e29bc9
Update ui_test.py
chrizzocb Jun 3, 2026
e1caf1b
Merge branch 'main' into Update-profile_models
chrizzocb Jun 3, 2026
7061492
update14
chrizzocb Jun 3, 2026
78a9491
update15
chrizzocb Jun 3, 2026
f1596d8
update16
chrizzocb Jun 3, 2026
29fccd0
update17
chrizzocb Jun 4, 2026
615cd4e
update for the common seal
chrizzocb Jun 4, 2026
e7a0067
Merge branch 'main' into Update-profile_models
cjshrader Jun 6, 2026
d4ac0f8
update 18
chrizzocb Jun 7, 2026
8e5c298
update19
chrizzocb Jun 9, 2026
cee048d
Merge branch 'd4lfteam:main' into Update-profile_models
chrizzocb Jun 9, 2026
5ca8ff2
Create __init__.py
chrizzocb Jun 9, 2026
67677ed
Update __init__.py
chrizzocb Jun 9, 2026
b5484ec
Rework of the base seal/charm model and cleaned up a bunch of extra c…
cjshrader Jun 12, 2026
f124c0c
update20
chrizzocb Jun 12, 2026
7c753f7
update21
chrizzocb Jun 12, 2026
dadb291
update22
chrizzocb Jun 12, 2026
9234c0d
Update charms.py
chrizzocb Jun 13, 2026
5fb28d1
importer update1
chrizzocb Jun 13, 2026
eae9bd4
importer update2
chrizzocb Jun 14, 2026
fa63dcc
comment resolving
chrizzocb Jun 14, 2026
9a2ca3c
charm set name update
chrizzocb Jun 14, 2026
9be3b3d
Reworked Maxroll imports to fix missing attributes and better handlin…
cjshrader Jun 14, 2026
b0128c5
Merge branch 'main' into Update-profile_models
cjshrader Jun 15, 2026
66fc742
mythic seals update
chrizzocb Jun 16, 2026
a4c6f9e
Small fixes based on comments
cjshrader Jun 17, 2026
a1383be
mobalytics update
chrizzocb Jun 18, 2026
1736151
update d4builds
chrizzocb Jun 18, 2026
962e567
Update maxroll.py
chrizzocb Jun 18, 2026
be73fab
updated gui_common.py
chrizzocb Jun 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/config/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ def validate_percent(v: int) -> int:
return v


def validate_greater_affix_count(v: int) -> int:
if not 0 <= v <= 4:
msg = "must be in [0, 4]"
raise ValueError(msg)
return v


def validate_hotkey(k: str) -> str:
keyboard.parse_hotkey(k)
return k
Expand Down
165 changes: 143 additions & 22 deletions src/config/profile_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)

Expand All @@ -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]]:
Comment thread
cjshrader marked this conversation as resolved.
Outdated
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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -207,6 +232,91 @@ def unique_aspect_names_must_be_unique(self) -> ItemFilterModel:
DynamicItemFilterModel = RootModel[dict[str, ItemFilterModel]]


class SealCharmFilterModel(BaseModel):
Comment thread
cjshrader marked this conversation as resolved.
Outdated
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")
Comment thread
cjshrader marked this conversation as resolved.
Outdated

@field_validator("unique_aspect")
@classmethod
def normalize_unique_aspect(cls, name: str | None) -> str | None:
return correct_name(name)
Comment thread
cjshrader marked this conversation as resolved.
Outdated


class BoostedSetFilterModel(BaseModel):
model_config = ConfigDict(extra="forbid", populate_by_name=True)
Comment thread
cjshrader marked this conversation as resolved.
Outdated
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"
Comment thread
cjshrader marked this conversation as resolved.
Outdated
raise ValueError(msg)
return self


class SealFilterModel(SealCharmFilterModel):
boosted_affix: AffixFilterModel | None = Field(default=None, alias="boostedAffix")
Comment thread
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")
Comment thread
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]]
Comment thread
cjshrader marked this conversation as resolved.
Outdated
DynamicCharmFilterModel = RootModel[dict[str, CharmFilterModel]]
DynamicSealFilterModel = RootModel[dict[str, SealFilterModel]]


class SigilPriority(enum.StrEnum):
blacklist = enum.auto()
whitelist = enum.auto()
Expand Down Expand Up @@ -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):
Comment thread
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
Expand All @@ -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"
)
Expand Down
6 changes: 6 additions & 0 deletions src/dataloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class Dataloader:
filter_after_keyword = []
filter_words = []
item_types_dict = {}
set_list = []
tooltips = {}
tribute_dict = {}

Expand Down Expand Up @@ -95,3 +96,8 @@ def load_data(self):
encoding="utf-8"
) as f:
self.aspect_unique_dict = json.load(f)

with pathlib.Path(BASE_DIR / f"assets/lang/{IniConfigLoader().general.language}/sets.json").open(
encoding="utf-8"
) as f:
self.set_list = json.load(f)
41 changes: 35 additions & 6 deletions src/gui/importer/d4builds.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,17 @@
AffixFilterCountModel,
AffixFilterModel,
Comment thread
cjshrader marked this conversation as resolved.
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,
Expand Down Expand Up @@ -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]:
Expand Down Expand Up @@ -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 = [
Expand All @@ -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),
Comment thread
cjshrader marked this conversation as resolved.
Outdated
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

Expand Down Expand Up @@ -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:
Comment thread
cjshrader marked this conversation as resolved.
Outdated
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"]
Expand Down
29 changes: 28 additions & 1 deletion src/gui/importer/gui_common.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,16 @@

from src import __version__
from src.config.loader import IniConfigLoader
from src.config.profile_models import AspectUniqueFilterModel, ItemFilterModel, ProfileModel
from src.config.profile_models import (
AffixFilterCountModel,
AffixFilterModel,
AspectUniqueFilterModel,
ItemFilterModel,
ProfileModel,
SealCharmFilterModel,
)
from src.config.settings_models import BrowserType
from src.item.data.affix import Affix, AffixType
from src.item.data.item_type import ItemType

if TYPE_CHECKING:
Expand Down Expand Up @@ -188,6 +196,25 @@ def update_mingreateraffixcount(item_filter: ItemFilterModel, require_gas: bool)
item_filter.min_greater_affix_count = 0


def create_seal_charm_filter(
affixes: list[Affix], rarity, require_gas: bool, model_type: type[SealCharmFilterModel] = SealCharmFilterModel
) -> SealCharmFilterModel:
seal_charm_filter = model_type(
affix_pool=[
AffixFilterCountModel(
count=[
AffixFilterModel(name=affix.name, want_greater=affix.type == AffixType.greater) for affix in affixes
],
min_count=min(3, len(affixes)),
)
],
rarities=[rarity] if rarity is not None else [],
)
if require_gas:
seal_charm_filter.min_greater_affix_count = len([affix for affix in affixes if affix.type == AffixType.greater])
return seal_charm_filter


def add_mythics_to_filters(mythic_names, finished_filters):
if mythic_names:
mythic_filter = ItemFilterModel()
Expand Down
Loading
Loading