Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
58 changes: 45 additions & 13 deletions src/config/profile_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from src.config.helper import check_greater_than_zero, 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,26 @@ def _parse_item_type_or_rarities(data: str | list[str]) -> list[str]:
return data


def _is_item_rarity(data: str) -> bool:
return any(rarity.value.lower() == data.lower() for rarity in ItemRarity)
Comment thread
cjshrader marked this conversation as resolved.
Outdated


def _parse_name_or_rarities(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 _is_item_rarity(data):
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)


class AffixAspectFilterModel(BaseModel):
model_config = ConfigDict(extra="forbid")
name: str
Expand Down Expand Up @@ -295,19 +316,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 _parse_name_or_rarities(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 _parse_name_or_rarities(data)

@field_validator("rarities", mode="before")
@classmethod
Expand All @@ -319,8 +349,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[NameRarityFilterModel] = Field(default=[], alias="Charms")
Comment thread
cjshrader marked this conversation as resolved.
Outdated
global_uniques: list[GlobalUniqueModel] = Field(default=[], alias="GlobalUniques")
name: str
seals: list[NameRarityFilterModel] = Field(default=[], alias="Seals")
sigils: SigilFilterModel = Field(
default=SigilFilterModel(blacklist=[], whitelist=[], priority=SigilPriority.blacklist), alias="Sigils"
)
Expand Down
4 changes: 4 additions & 0 deletions src/item/data/item_type.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ def is_sigil(item_type: ItemType) -> bool:
return item_type in [ItemType.Sigil, ItemType.EscalationSigil]


def is_horadric_spellcraft(item_type: ItemType) -> bool:
Comment thread
cjshrader marked this conversation as resolved.
Outdated
return item_type in [ItemType.HoradricSeal, ItemType.Charm]


def is_jewelry(item_type: ItemType) -> bool:
return item_type in [ItemType.Amulet, ItemType.Ring]

Expand Down
5 changes: 4 additions & 1 deletion src/item/descr/read_descr_tts.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
ItemType,
is_armor,
is_consumable,
is_horadric_spellcraft,
is_jewelry,
is_non_sigil_mapping,
is_sigil,
Expand Down Expand Up @@ -276,7 +277,7 @@ def _create_base_item_from_tts(tts_item: list[str]) -> Item | None:
starting_item_type_index = 1
if item.rarity == ItemRarity.Mythic:
starting_item_type_index = 2
elif item.rarity == ItemRarity.Common:
elif item.rarity == ItemRarity.Common and search_string_split[0] != ItemRarity.Common.value:
Comment thread
cjshrader marked this conversation as resolved.
Outdated
starting_item_type_index = 0
item.item_type = _get_item_type(" ".join(search_string_split[starting_item_type_index:]))
item.name = correct_name(tts_item[0])
Expand Down Expand Up @@ -451,6 +452,7 @@ def read_descr_mixed(img_item_descr: np.ndarray) -> Item | None:
if any([
is_consumable(item.item_type),
is_non_sigil_mapping(item.item_type),
is_horadric_spellcraft(item.item_type),
is_sigil(item.item_type),
is_socketable(item.item_type),
item.item_type in [ItemType.Material, ItemType.Tribute],
Expand Down Expand Up @@ -501,6 +503,7 @@ def read_descr() -> Item | None:
if any([
is_consumable(item.item_type),
is_non_sigil_mapping(item.item_type),
is_horadric_spellcraft(item.item_type),
is_socketable(item.item_type),
item.item_type in [ItemType.Material, ItemType.Tribute, ItemType.Cache, ItemType.LairBossKey],
item.seasonal_attribute == SeasonalAttribute.sanctified,
Expand Down
56 changes: 47 additions & 9 deletions src/item/filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
AffixFilterModel,
DynamicItemFilterModel,
GlobalUniqueModel,
NameRarityFilterModel,
ProfileModel,
SigilConditionModel,
SigilFilterModel,
Expand Down Expand Up @@ -67,6 +68,8 @@ class Filter:
aspect_upgrade_filters = {}
paragon_filters = {}
global_unique_filters = {}
seal_filters = {}
charm_filters = {}
sigil_filters = {}
tribute_filters = {}

Expand Down Expand Up @@ -236,31 +239,52 @@ def _check_sigil(self, item: Item) -> FilterResult:
res.matched.append(MatchedFilter(f"{profile_name}"))
return res

def _check_tribute(self, item: Item) -> FilterResult:
def _check_name_rarity_filters(
self,
item: Item,
item_filters: dict[str, list[NameRarityFilterModel | TributeFilterModel]],
section_name: str,
mythic_name: str,
) -> FilterResult:
res = FilterResult(keep=False, matched=[])
if not self.tribute_filters.items():
LOGGER.info(f"{item.original_name} -- Matched Tributes")
if not item_filters.items():
LOGGER.info(f"{item.original_name} -- Matched {section_name}")
Comment thread
cjshrader marked this conversation as resolved.
Outdated
res.keep = True
res.matched.append(MatchedFilter("Tributes not filtered"))
res.matched.append(MatchedFilter(f"{section_name} not filtered"))

if item.rarity == ItemRarity.Mythic:
LOGGER.info(f"{item.original_name} -- Matched mythic tribute, always kept")
LOGGER.info(f"{item.original_name} -- Matched mythic {section_name.lower()}, always kept")
Comment thread
cjshrader marked this conversation as resolved.
Outdated
res.keep = True
res.matched.append(MatchedFilter("Mythic Tribute"))
res.matched.append(MatchedFilter(mythic_name))

for profile_name, profile_filter in self.tribute_filters.items():
for profile_name, profile_filter in item_filters.items():
for filter_item in profile_filter:
if filter_item.name and not item.name.startswith(filter_item.name):
if filter_item.name and not (item.name or "").startswith(filter_item.name):
continue

if filter_item.rarities and item.rarity not in filter_item.rarities:
continue

LOGGER.info(f"{item.original_name} -- Matched {profile_name}.Tributes")
LOGGER.info(f"{item.original_name} -- Matched {profile_name}.{section_name}")
res.keep = True
res.matched.append(MatchedFilter(f"{profile_name}"))
return res

def _check_tribute(self, item: Item) -> FilterResult:
Comment thread
cjshrader marked this conversation as resolved.
return self._check_name_rarity_filters(
item=item, item_filters=self.tribute_filters, section_name="Tributes", mythic_name="Mythic Tribute"
)

def _check_seal(self, item: Item) -> FilterResult:
return self._check_name_rarity_filters(
item=item, item_filters=self.seal_filters, section_name="Seals", mythic_name="Mythic Seal"
)

def _check_charm(self, item: Item) -> FilterResult:
return self._check_name_rarity_filters(
item=item, item_filters=self.charm_filters, section_name="Charms", mythic_name="Mythic Charm"
)

def _check_global_unique_filter(self, item: Item) -> FilterResult:
res = FilterResult(keep=False, matched=[])

Expand Down Expand Up @@ -479,6 +503,8 @@ def load_files(self):
self.affix_filters: dict[str, list[DynamicItemFilterModel]] = {}
self.aspect_upgrade_filters: dict[str, list[str]] = {}
self.paragon_filters: dict[str, object] = {}
self.seal_filters: dict[str, list[NameRarityFilterModel]] = {}
self.charm_filters: dict[str, list[NameRarityFilterModel]] = {}
self.sigil_filters: dict[str, SigilFilterModel] = {}
self.tribute_filters: dict[str, list[TributeFilterModel]] = {}
self.global_unique_filters: dict[str, list[GlobalUniqueModel]] = {}
Expand Down Expand Up @@ -539,6 +565,12 @@ def load_files(self):
if data.aspect_upgrades:
self.aspect_upgrade_filters[data.name] = data.aspect_upgrades
sections.append(ASPECT_UPGRADES_LABEL)
if data.seals:
self.seal_filters[data.name] = data.seals
sections.append("Seals")
if data.charms:
self.charm_filters[data.name] = data.charms
sections.append("Charms")
if data.sigils and (data.sigils.blacklist or data.sigils.whitelist):
self.sigil_filters[data.name] = data.sigils
sections.append("Sigils")
Expand Down Expand Up @@ -575,6 +607,12 @@ def should_keep(self, item: Item) -> FilterResult:
if item.item_type == ItemType.Tribute:
return self._check_tribute(item)

if item.item_type == ItemType.HoradricSeal:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L674/682: 🔵 nit: dispatches on raw item.item_type == ItemType.HoradricSeal/Charm — inconsistent with is_sigil() used 6 lines above. Wrap in is_seal_or_charm() and dispatch inside, or add is_seal()/is_charm() helpers.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not correct. is_sigil checks across two types. We need different handling per type. The better example is if item.item_type == ItemType.Tribute which is exactly what we're mirroring here.

return self._check_seal(item)

if item.item_type == ItemType.Charm:
return self._check_charm(item)

if item.item_type is None or item.power is None:
return res

Expand Down
2 changes: 2 additions & 0 deletions src/scripts/vision_mode_with_highlighting.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,8 @@ def evaluate_item_and_queue_draw(self, item_descr: Item):
item_descr_with_loc = item_descr
else:
item_descr_with_loc = src.item.descr.read_descr_tts.read_descr_mixed(cropped_descr)
if item_descr_with_loc is None:
Comment thread
cjshrader marked this conversation as resolved.
Outdated
item_descr_with_loc = item_descr
res = Filter().should_keep(item_descr_with_loc)
match = res.keep

Expand Down
44 changes: 44 additions & 0 deletions tests/config/models_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
GlobalUniqueModel,
ItemFilterModel,
ItemRarity,
NameRarityFilterModel,
ProfileModel,
SigilConditionModel,
SigilFilterModel,
Expand Down Expand Up @@ -590,6 +591,31 @@ def test_rarities_parse_list(self) -> None:
assert ItemRarity.Legendary in model.rarities


class TestNameRarityFilterModel:
"""Test generic name and rarity filters."""

def test_name_is_normalized(self) -> None:
"""Test item names are normalized for matching TTS item names."""
model = NameRarityFilterModel.model_validate("Faint Charm")
assert model.name == "faint_charm"

def test_parse_from_string_rarity(self) -> None:
"""Test parsing every rarity string."""
for rarity in ItemRarity:
model = NameRarityFilterModel.model_validate(rarity.value)
assert rarity in model.rarities

def test_parse_from_list(self) -> None:
"""Test parsing from a rarity list."""
model = NameRarityFilterModel.model_validate([ItemRarity.Legendary.value, ItemRarity.Unique.value])
assert model.rarities == [ItemRarity.Legendary, ItemRarity.Unique]

def test_parse_empty_list_fails(self) -> None:
"""Test that empty lists fail."""
with pytest.raises(ValidationError, match="list cannot be empty"):
NameRarityFilterModel.model_validate([])


class TestSigilConditionModel:
"""Test SigilConditionModel."""

Expand Down Expand Up @@ -669,13 +695,17 @@ def test_camelcase_input(self) -> None:
GlobalUniques=[GlobalUniqueModel(minPower=800)],
Sigils={"blacklist": [], "whitelist": [], "priority": "blacklist"},
Tributes=[],
Seals=["legendary"],
Charms=["rare"],
Paragon=None,
)
assert model.name == "test_profile"
assert model.affixes == []
assert model.aspect_upgrades == []
assert len(model.global_uniques) == 1
assert model.global_uniques[0].min_power == 800
assert model.seals[0].rarities == [ItemRarity.Legendary]
assert model.charms[0].rarities == [ItemRarity.Rare]

def test_snake_case_input(self) -> None:
"""Test loading with snake_case (new format)."""
Expand All @@ -686,13 +716,17 @@ def test_snake_case_input(self) -> None:
global_uniques=[GlobalUniqueModel(min_power=900)],
sigils={"blacklist": [], "whitelist": [], "priority": "blacklist"},
tributes=[],
seals=["faint seal"],
charms=["faint charm"],
paragon=None,
)
assert model.name == "test_profile"
assert model.affixes == []
assert model.aspect_upgrades == []
assert len(model.global_uniques) == 1
assert model.global_uniques[0].min_power == 900
assert model.seals[0].name == "faint_seal"
assert model.charms[0].name == "faint_charm"

def test_mixed_naming(self) -> None:
"""Test mixing both naming conventions."""
Expand Down Expand Up @@ -747,6 +781,8 @@ def test_export_snake_case(self) -> None:
assert "affixes" in exported
assert "aspect_upgrades" in exported
assert "global_uniques" in exported
assert "seals" in exported
assert "charms" in exported
assert "sigils" in exported
assert "tributes" in exported
assert "paragon" in exported
Expand All @@ -758,6 +794,8 @@ def test_export_snake_case(self) -> None:
assert "Affixes" not in exported
assert "AspectUpgrades" not in exported
assert "GlobalUniques" not in exported
assert "Seals" not in exported
assert "Charms" not in exported
assert "minPower" not in exported["global_uniques"][0]

def test_export_camelcase(self) -> None:
Expand All @@ -769,6 +807,8 @@ def test_export_camelcase(self) -> None:
assert "Affixes" in exported
assert "AspectUpgrades" in exported
assert "GlobalUniques" in exported
assert "Seals" in exported
assert "Charms" in exported
assert "Sigils" in exported
assert "Tributes" in exported
assert "Paragon" in exported
Expand All @@ -780,6 +820,8 @@ def test_export_camelcase(self) -> None:
assert "affixes" not in exported
assert "aspect_upgrades" not in exported
assert "global_uniques" not in exported
assert "seals" not in exported
assert "charms" not in exported
assert "min_power" not in exported["GlobalUniques"][0]

def test_defaults(self) -> None:
Expand All @@ -790,6 +832,8 @@ def test_defaults(self) -> None:
assert model.affixes == []
assert model.aspect_upgrades == []
assert model.global_uniques == []
assert model.seals == []
assert model.charms == []
assert model.tributes == []
assert model.paragon is None
assert model.sigils.blacklist == []
Expand Down
Loading
Loading