Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cleanup + Allow greater affixes #403

Merged
merged 2 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading