Skip to content

Commit

Permalink
Cleanup + Allow greater affixes (#403)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisoro authored Oct 23, 2024
1 parent a4b9e6c commit 02aedb9
Show file tree
Hide file tree
Showing 18 changed files with 297 additions and 412 deletions.
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ repos:
- id: trailing-whitespace

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.6.9
rev: v0.7.0
hooks:
- id: ruff
args: [--fix]
Expand Down
5 changes: 2 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,9 @@ To use TTS, you need to:

Currently, `use_tts` enables either a mixed mode where image processing is still used for item and affix position detection,
but TTS is used for everything text-related. This results in a small improvement in performance and a major improvement
in accuracy. Or a full mode where only TTS is used.
in accuracy. Or a full mode where only TTS is used. This will be super fast but loses the overlay.

**IT IS NOT POSSIBLE TO DETECT GREATER AFFIXES USING TTS!!! YOU NEED TO SPECIFY THEM USING THEIR VALUES!**
**YOU NEED TO ENABLE ADVANCED TOOLTIP INFORMATION**

**The following is currently supported using any form of tts:**

Expand Down Expand Up @@ -108,7 +108,6 @@ The config folder in `C:/Users/<WINDOWS_USER>/.d4lf` contains:
| full_dump | When using the import build feature, whether to use the full dump (e.g. contains all filter items) or not |
| handle_rares | - `filter`: Filter them based on your profiles <br>- `ignore`: Ignores all rares, vision mode shows them as blue and auto mode never junks or favorites them <br>- `junk`: Vision mode shows them always as red, auto mode always junks rares |
| handle_uniques | How to handle uniques that do not match any filter. This property does not apply to filtered uniques. All mythics are favorited regardless of filter. <br/>- `favorite`: Mark the unique as favorite and vision mode will show it as green (default)<br/>- `ignore`: Do nothing with the unique and vision mode will show it as green<br/>- `junk`: Mark any uniques that don't match any filters as junk and show as red in vision mode |
| 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 |
| keep_aspects | - `all`: Keep all legendary items <br>- `upgrade`: Keep all legendary items that upgrade your codex of power <br>- `none`: Keep no legendary items based on aspect (they are still filtered!) |
| mark_as_favorite | Whether to favorite matched items or not. Defaults to true |
| minimum_overlay_font_size | The minimum font size for the vision overlay, specifically the green text that shows which filter(s) are matching. Note: For small profile names, the font may actually be larger than this size but will never go below this size. |
Expand Down
4 changes: 1 addition & 3 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
./dependencies/tesserocr-2.7.0-cp312-cp312-win_amd64.whl
beautifultable
colorama
coverage
cryptography
httpx
keyboard
lxml
mouse
mss
natsort
numpy<2
opencv-python==4.10.0.82
opencv-python==4.10.0.84
Pillow
pre-commit
psutil
Expand Down
2 changes: 1 addition & 1 deletion src/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

TP = concurrent.futures.ThreadPoolExecutor()

__version__ = "5.9.0alpha3"
__version__ = "5.9.0alpha4"
10 changes: 1 addition & 9 deletions src/config/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
IS_HOTKEY_KEY = "is_hotkey"

DEPRECATED_INI_KEYS = [
"hidden_transparency",
"import_build",
"local_prefs_path",
"move_item_type",
Expand Down Expand Up @@ -253,9 +254,6 @@ class GeneralModel(_IniBaseModel):
default=UnfilteredUniquesType.favorite,
description="What should be done with uniques that do not match any profile. Mythics are always favorited. If mark_as_favorite is unchecked then uniques that match a profile will not be favorited.",
)
hidden_transparency: float = Field(
default=0.35, description="Transparency of the overlay when not hovering it (has a 3 second delay after hovering)"
)
keep_aspects: AspectFilterType = Field(
default=AspectFilterType.upgrade, description="Whether to keep aspects that didn't match a filter"
)
Expand Down Expand Up @@ -309,12 +307,6 @@ def language_must_exist(cls, v: str) -> str:
raise ValueError("language not supported")
return v

@field_validator("hidden_transparency")
def transparency_in_range(cls, v: float) -> float:
if not 0 <= v <= 1:
raise ValueError("must be in [0, 1]")
return v

@field_validator("minimum_overlay_font_size")
def font_size_in_range(cls, v: int) -> int:
if not 10 <= v <= 20:
Expand Down
6 changes: 4 additions & 2 deletions src/item/data/affix.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ class AffixType(enum.Enum):

@dataclass
class Affix:
name: str
type: AffixType = AffixType.normal
loc: tuple[int, int] = None
max_value: float | None = None
min_value: float | None = None
name: str = ""
text: str = ""
type: AffixType = AffixType.normal
value: float | None = None

def __eq__(self, other: "Affix") -> bool:
Expand Down
2 changes: 2 additions & 0 deletions src/item/descr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def keep_letters_and_spaces(text):
return "".join(char for char in text if char.isalpha() or char.isspace()).strip().replace(" ", " ")
180 changes: 104 additions & 76 deletions src/item/descr/read_descr_tts.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
import logging
import re

import numpy as np
import rapidfuzz
Expand All @@ -12,15 +13,63 @@
from src.item.data.aspect import Aspect
from src.item.data.item_type import ItemType, is_armor, is_consumable, is_jewelry, is_mapping, is_socketable, is_weapon
from src.item.data.rarity import ItemRarity
from src.item.descr import keep_letters_and_spaces
from src.item.descr.text import clean_str, closest_match, find_number
from src.item.descr.texture import find_affix_bullets, find_seperator_short, find_seperators_long
from src.item.models import Item
from src.template_finder import TemplateMatch
from src.utils.window import screenshot

_AFFIX_RE = re.compile(
r"(?P<affixvalue1>[0-9]+)[^0-9]+\[(?P<minvalue1>[0-9]+) - (?P<maxvalue1>[0-9]+)\]|"
r"(?P<affixvalue2>[0-9]+\.[0-9]+).+?\[(?P<minvalue2>[0-9]+\.[0-9]+) - (?P<maxvalue2>[0-9]+\.[0-9]+)\]|"
r"(?P<affixvalue3>[.0-9]+)[^0-9]+\[(?P<onlyvalue>[.0-9]+)\]|"
r".?![^\[\]]*[\[\]](?P<affixvalue4>\d+.?:\.\d+?)(?P<greateraffix1>[ ]*)|"
r"(?P<greateraffix2>\d+)(?![^\[]*\[).*",
re.DOTALL,
)

_AFFIX_REPLACEMENTS = [
"%",
"+",
",",
"[+]",
"[x]",
]
LOGGER = logging.getLogger(__name__)


def _add_affixes_from_tts(tts_section: list[str], item: Item) -> Item:
inherent_num = 0
affixes_num = 3 if item.rarity == ItemRarity.Legendary else 4
if is_weapon(item.item_type) or item.item_type in [ItemType.Amulet, ItemType.Boots]:
inherent_num = 1
elif item.item_type in [ItemType.Ring]:
inherent_num = 2
elif item.item_type in [ItemType.Shield]:
inherent_num = 4
affixes = _get_affixes_from_tts_section(tts_section, item, inherent_num + affixes_num)
for i, affix_text in enumerate(affixes):
if i < inherent_num:
affix = Affix(text=affix_text)
affix.type = AffixType.inherent
affix.name = rapidfuzz.process.extractOne(
keep_letters_and_spaces(affix_text), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)[0]
item.inherent.append(affix)
elif i < inherent_num + affixes_num:
affix = _get_affix_from_text(affix_text)
item.affixes.append(affix)
else:
name = closest_match(clean_str(affix_text)[:AFFIX_COMPARISON_CHARS], Dataloader().aspect_unique_dict)
item.aspect = Aspect(
name=name,
text=affix_text,
value=find_number(affix_text),
)
return item


def _add_affixes_from_tts_mixed(
tts_section: list[str], item: Item, inherent_affix_bullets: list[TemplateMatch], affix_bullets: list[TemplateMatch]
) -> Item:
Expand All @@ -30,91 +79,32 @@ def _add_affixes_from_tts_mixed(
len(inherent_affix_bullets)
+ len([x for x in affix_bullets if any(x.name.startswith(s) for s in ["affix", "greater_affix", "rerolled"])]),
)
for i, affix in enumerate(affixes):
for i, affix_text in enumerate(affixes):
if i < len(inherent_affix_bullets):
name = rapidfuzz.process.extractOne(
clean_str(affix), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)
item.inherent.append(
Affix(
name=name[0],
loc=inherent_affix_bullets[i].center,
text=affix,
type=AffixType.inherent,
value=find_number(affix),
)
)
affix = Affix(text=affix_text)
affix.type = AffixType.inherent
affix.name = rapidfuzz.process.extractOne(
keep_letters_and_spaces(affix_text), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)[0]
affix.loc = (inherent_affix_bullets[i].center,)
item.inherent.append(affix)
elif i < len(inherent_affix_bullets) + len(affix_bullets):
name = rapidfuzz.process.extractOne(
clean_str(affix), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)
affix = _get_affix_from_text(affix_text)
affix.loc = (affix_bullets[i - len(inherent_affix_bullets)].center,)
if affix_bullets[i - len(inherent_affix_bullets)].name.startswith("greater_affix"):
a_type = AffixType.greater
affix.type = AffixType.greater
elif affix_bullets[i - len(inherent_affix_bullets)].name.startswith("rerolled"):
a_type = AffixType.rerolled
affix.type = AffixType.rerolled
else:
a_type = AffixType.normal
item.affixes.append(
Affix(
name=name[0],
loc=affix_bullets[i - len(inherent_affix_bullets)].center,
text=affix,
type=a_type,
value=find_number(affix),
)
)
affix.type = AffixType.normal
item.affixes.append(affix)
else:
name = closest_match(clean_str(affix)[:AFFIX_COMPARISON_CHARS], Dataloader().aspect_unique_dict)
name = closest_match(clean_str(affix_text)[:AFFIX_COMPARISON_CHARS], Dataloader().aspect_unique_dict)
item.aspect = Aspect(
name=name,
loc=affix_bullets[i - len(inherent_affix_bullets) - len(affix_bullets)].center,
text=affix,
value=find_number(affix),
)
return item


def _add_affixes_from_tts(tts_section: list[str], item: Item) -> Item:
inherent_num = 0
affixes_num = 3 if item.rarity == ItemRarity.Legendary else 4
if is_weapon(item.item_type) or item.item_type in [ItemType.Amulet, ItemType.Boots]:
inherent_num = 1
elif item.item_type in [ItemType.Ring]:
inherent_num = 2
elif item.item_type in [ItemType.Shield]:
inherent_num = 4
affixes = _get_affixes_from_tts_section(tts_section, item, inherent_num + affixes_num)
for i, affix in enumerate(affixes):
if i < inherent_num:
name = rapidfuzz.process.extractOne(
clean_str(affix), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)
item.inherent.append(
Affix(
name=name[0],
text=affix,
type=AffixType.inherent,
value=find_number(affix),
)
)
elif i < inherent_num + affixes_num:
name = rapidfuzz.process.extractOne(
clean_str(affix), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)
item.affixes.append(
Affix(
name=name[0],
text=affix,
type=AffixType.normal,
value=find_number(affix),
)
)
else:
name = closest_match(clean_str(affix)[:AFFIX_COMPARISON_CHARS], Dataloader().aspect_unique_dict)
item.aspect = Aspect(
name=name,
text=affix,
value=find_number(affix),
text=affix_text,
value=find_number(affix_text),
)
return item

Expand Down Expand Up @@ -190,6 +180,42 @@ def _get_affixes_from_tts_section(tts_section: list[str], item: Item, length: in
return tts_section[start : start + length]


def _get_affix_from_text(text: str) -> Affix:
result = Affix(text=text)
for x in _AFFIX_REPLACEMENTS:
text = text.replace(x, "")
matched_groups = {}
for match in _AFFIX_RE.finditer(text):
matched_groups = {name: value for name, value in match.groupdict().items() if value is not None}
if not matched_groups:
raise Exception(f"Could not match affix text: {text}")
for x in ["minvalue1", "minvalue2"]:
if matched_groups.get(x) is not None:
result.min_value = float(matched_groups[x])
break
for x in ["maxvalue1", "maxvalue2"]:
if matched_groups.get(x) is not None:
result.max_value = float(matched_groups[x])
break
for x in ["affixvalue1", "affixvalue2", "affixvalue3", "affixvalue4"]:
if matched_groups.get(x) is not None:
result.value = float(matched_groups[x])
break
for x in ["greateraffix1", "greateraffix2"]:
if matched_groups.get(x) is not None:
result.type = AffixType.greater
if x == "greateraffix2":
result.value = float(matched_groups[x])
break
if matched_groups.get("onlyvalue") is not None:
result.min_value = float(matched_groups.get("onlyvalue"))
result.max_value = float(matched_groups.get("onlyvalue"))
result.name = rapidfuzz.process.extractOne(
keep_letters_and_spaces(text), list(Dataloader().affix_dict), scorer=rapidfuzz.distance.Levenshtein.distance
)[0]
return result


def _get_item_rarity(data: str) -> ItemRarity | None:
res = rapidfuzz.process.extractOne(data, [rar.value for rar in ItemRarity], scorer=rapidfuzz.distance.Levenshtein.distance)
try:
Expand Down Expand Up @@ -294,6 +320,8 @@ def read_descr() -> Item | None:
return item
if all([not is_armor(item.item_type), not is_jewelry(item.item_type), not is_weapon(item.item_type)]):
return None
if item.rarity not in [ItemRarity.Legendary, ItemRarity.Mythic, ItemRarity.Unique]:
return item

item.codex_upgrade = _is_codex_upgrade(tts_section, item)
return _add_affixes_from_tts(tts_section, item)
23 changes: 5 additions & 18 deletions src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,19 @@
import time
import traceback

import keyboard
from beautifultable import BeautifulTable
from PIL import Image # noqa # Note: Somehow needed, otherwise the binary has an issue with tesserocr

import src.logger
from src import __version__, tts
from src.cam import Cam
from src.config.loader import IniConfigLoader
from src.config.models import ItemRefreshType, UseTTSType
from src.config.models import UseTTSType
from src.gui.qt_gui import start_gui
from src.item.filter import Filter
from src.logger import LOG_DIR
from src.overlay import Overlay
from src.utils.process_handler import safe_exit
from src.scripts.handler import ScriptHandler
from src.utils.window import WindowSpec, start_detecting_window

LOGGER = logging.getLogger(__name__)
Expand Down Expand Up @@ -55,24 +54,12 @@ def main():
while not Cam().is_offset_set():
time.sleep(0.2)

overlay = None
ScriptHandler()

keyboard.add_hotkey(IniConfigLoader().advanced_options.run_scripts, lambda: overlay.run_scripts() if overlay is not None else None)
keyboard.add_hotkey(IniConfigLoader().advanced_options.exit_key, lambda: safe_exit())
if not IniConfigLoader().advanced_options.vision_mode_only:
keyboard.add_hotkey(IniConfigLoader().advanced_options.run_filter, lambda: overlay.filter_items() if overlay is not None else None)
keyboard.add_hotkey(
IniConfigLoader().advanced_options.run_filter_force_refresh,
lambda: overlay.filter_items(ItemRefreshType.force_with_filter) if overlay is not None else None,
)
keyboard.add_hotkey(
IniConfigLoader().advanced_options.force_refresh_only,
lambda: overlay.filter_items(ItemRefreshType.force_without_filter) if overlay is not None else None,
)
keyboard.add_hotkey(IniConfigLoader().advanced_options.move_to_inv, lambda: overlay.move_items_to_inventory())
keyboard.add_hotkey(IniConfigLoader().advanced_options.move_to_chest, lambda: overlay.move_items_to_stash())
if IniConfigLoader().general.use_tts in [UseTTSType.full, UseTTSType.mixed]:
LOGGER.debug(f"TTS mode: {IniConfigLoader().general.use_tts.value}")
tts.start_connection()

overlay = Overlay()
overlay.run()

Expand Down
Loading

0 comments on commit 02aedb9

Please sign in to comment.