diff --git a/README.md b/README.md
index ecc8591d..c3897add 100644
--- a/README.md
+++ b/README.md
@@ -1,103 +1,144 @@
# 
-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.
]
## 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 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: [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 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
# 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/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 27ba54eb..c499fd86 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").split(","),
@@ -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 4e2d5dec..1b49b922 100644
--- a/src/config/models.py
+++ b/src/config/models.py
@@ -1,20 +1,103 @@
"""New config loading and verification using pydantic. For now, both will exist in parallel hence _new."""
+import enum
+import sys
from pathlib import Path
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
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):
+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
+
+ @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 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"
process_name: str = "Diablo IV.exe"
@@ -23,7 +106,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 +123,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 +131,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: list[int]
hidden_transparency: float
language: str = "enUS"
@@ -91,7 +174,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)
@@ -105,7 +188,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]:
@@ -125,7 +208,78 @@ def values_in_range(cls, v: numpy.ndarray) -> numpy.ndarray:
return v
-class UiOffsets(_IniBase):
+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 = sys.maxsize
+ 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[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]:
+ return _parse_item_type(data)
+
+
+class ProfileModel(BaseModel):
+ name: str
+ Affixes: list[DynamicItemFilterModel] = []
+ Aspects: list[AspectFilterModel] = []
+ 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
@@ -135,12 +289,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..74b39219 100644
--- a/src/dataloader.py
+++ b/src/dataloader.py
@@ -1,5 +1,7 @@
import json
+import os
import threading
+from pathlib import Path
from config.loader import IniConfigLoader
from item.data.item_type import ItemType
@@ -31,42 +33,32 @@ 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(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(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(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(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}/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(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__:
+ 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(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"],
@@ -75,14 +67,12 @@ 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(os.curdir) / f"assets/lang/{IniConfigLoader().general.language}/tooltips.json", "r", encoding="utf-8") as f:
self.tooltips = json.load(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
+ self.aspect_unique_dict[key] = d["desc"][:45]
+ self.aspect_unique_num_idx[key] = d["num_idx"]
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 df035e2a..2fc3c08f 100644
--- a/src/item/filter.py
+++ b/src/item/filter.py
@@ -1,13 +1,24 @@
import os
+import sys
import time
from dataclasses import dataclass, field
from pathlib import Path
-from typing import Any
import yaml
+from pydantic import ValidationError
from config.loader import IniConfigLoader
-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
@@ -47,46 +58,158 @@ 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
- 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_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
+ # 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.unique_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
user_dir = os.path.expanduser("~")
@@ -94,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"
@@ -119,281 +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 "
- if config["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
-
- 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"
- if config["Uniques"] is None:
- 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)}")
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(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]:
- return False
- # check value
- if len(filter_data["aspect"]) > 1:
- if 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)
- ):
- 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
-
- @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 _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
- )
- power_ok = self._check_item_power(filter_data, item.power)
- type_ok = self._check_item_type(filter_data, 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:
- 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"):
- continue
- # check affix
- if "blacklist" in profile_filter 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):
- 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:
- # 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):
- continue
- # check item power
- if not self._check_item_power(filter_item, item.power):
- continue
- # check aspect
- if item.aspect is None or not self._check_item_aspect(filter_item, item.aspect):
- continue
- # check affixes
- 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..e10475ac 100644
--- a/src/loot_filter.py
+++ b/src/loot_filter.py
@@ -65,16 +65,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 31a3630d..01d40d46 100644
--- a/src/main.py
+++ b/src/main.py
@@ -1,9 +1,9 @@
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 PIL import Image # noqa Somehow needed, otherwise the binary has an issue with tesserocr
from beautifultable import BeautifulTable
from cam import Cam
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
new file mode 100644
index 00000000..df029035
--- /dev/null
+++ b/test/config/data/sigils.py
@@ -0,0 +1,32 @@
+all_bad_cases = [
+ # 1 item
+ {"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
+ {"Sigils": {"blacklist": ["monster_cold_resist"]}},
+ {"Sigils": {"maxTier": 90}},
+ {"Sigils": {"minTier": 10}},
+ {"Sigils": {"whitelist": ["monster_cold_resist"]}},
+ # 2 items
+ {"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
+ {"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/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/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 f62dd017..d8684bae 100644
--- a/test/item/filter/data/filters.py
+++ b/test/item/filter/data/filters.py
@@ -1,112 +1,150 @@
-affix = {
- "test": [
+from config.models import (
+ UniqueModel,
+ ComparisonType,
+ ProfileModel,
+ SigilModel,
+ AffixFilterModel,
+ AspectUniqueFilterModel,
+ AspectFilterModel,
+ AffixFilterCountModel,
+ ItemFilterModel,
+)
+from item.data.item_type import ItemType
+
+# 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"],
- ]
-}
-sigil = {
- "test": {
- "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],
+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),
+)
+
+unique = ProfileModel(
+ name="test",
+ Uniques=[
+ UniqueModel(itemType=[ItemType.Scythe, ItemType.Sword], minPower=900),
+ UniqueModel(itemType=[ItemType.Scythe], minPower=900),
+ UniqueModel(
+ affix=[
+ 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": ["soulbrand", 20],
- "minPower": 900,
- "affixPool": [["attack_speed", 8.4]],
- },
- {
- "aspect": ["soulbrand", 15, "smaller"],
- },
- ]
-}
+ aspect=AspectUniqueFilterModel(name="lidless_wall", value=20),
+ minPower=900,
+ ),
+ UniqueModel(
+ affix=[AffixFilterModel(name="attack_speed", value=8.4)],
+ aspect=AspectUniqueFilterModel(name="soulbrand", value=20),
+ minPower=900,
+ ),
+ 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 d4254618..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,30 +19,30 @@ 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)
@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)