10 Commits

Author SHA1 Message Date
mgleed
c1f2cad291 Bugfix: Game recovery after D2 restart (#620)
* init

* bump version
2022-03-09 19:42:20 -05:00
mgleed
285e635b0d Bugfix: Some vendor items obscured by tooltips 2022-03-09 15:40:04 -05:00
mgleed
54203ff036 Bugfix: Chicken if stats/skill icons pop up to avoid accidental allocations (#618)
* init

* simplify

* undo
2022-03-09 15:39:44 -05:00
mgleed
6b378a4606 Bugfix: Avoid entering town portals while walking in town 2022-03-09 15:39:02 -05:00
mgleed
9707fc4563 Enhancement: Cleanup botty startup. Add version to debug log. 2022-03-09 13:30:06 -05:00
mgleed
1a4e6c9533 Bugfix: Gold stashing (#615)
* init

* more places to toggle inventory gold full off
2022-03-08 21:47:13 -05:00
mgleed
fb22b86d11 Bugfix: Stash is full message error (#614)
* fix item_cropper.crop_item_descr to allow some edge cases where there is lots of text in the box

* Fix send_stash message when stash is full

* remove self

* undo
2022-03-08 21:46:02 -05:00
mgleed
3c3f95142d Bugfix: crop_item_descr failing on some items 2022-03-08 21:45:37 -05:00
mgleed
029b8dcdc6 Bugfix: Lack of item tooltip causing item_cropper errors for users who turn it off (#612)
* Bugfix: Lack of item tooltip causing item_cropper errors.

* lol
2022-03-06 21:24:15 -05:00
mgleed
400d43c49c Bugfix: Tome discrepancies on some platforms (#611) 2022-03-06 20:55:50 -05:00
27 changed files with 87 additions and 51 deletions

View File

@@ -45,5 +45,6 @@
"Always Run": 1,
"Quick Cast Enabled": 0,
"Display Active Skill Bindings": 0,
"Lobby Wide Item Drop Enabled": 1
"Lobby Wide Item Drop Enabled": 1,
"Item Tooltip Hotkey Appender": 1
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

View File

@@ -129,6 +129,7 @@ left_panel_header=0,0,450,54
right_panel_header=830,0,450,54
npc_dialogue=458,4,21,140
bind_skill=516,619,251,29
quest_skill_btn=284,455,710,121
[path]
; static pathes in format: x0,y0, x1,y1, x2,y2, ...

View File

@@ -331,7 +331,7 @@ class Bot:
return self.trigger_or_stop("end_game", failed=True)
# Stash stuff
if keep_items:
if keep_items or personal.get_inventory_gold_full():
Logger.info("Stashing items")
self._curr_loc, result_items = self._town_manager.stash(self._curr_loc, items=items)
Logger.info("Running transmutes")

View File

@@ -170,7 +170,6 @@ class Config:
self.items["misc_gold"].pickit_type = 1
def load_data(self):
Logger.info("Loading Config Data")
self._config = configparser.ConfigParser()
self._config.read('config/params.ini')
self._game_config = configparser.ConfigParser()

View File

@@ -12,7 +12,7 @@ from game_stats import GameStats
from health_manager import HealthManager
from logger import Logger
from messages import Messenger
from screen import grab, found_offsets
from screen import grab, get_offset_state
from utils.restart import restart_game, kill_game
from utils.misc import kill_thread, set_d2r_always_on_top, restore_d2r_window_visibility
@@ -121,7 +121,7 @@ class GameController:
self.is_running = False
def setup_screen(self):
if found_offsets:
if get_offset_state():
return True
return False

View File

@@ -76,9 +76,7 @@ class HealthManager:
# Wait until the flag is reset by main.py
if self._did_chicken or self._pausing: continue
img = grab()
# TODO: Check if in town or not! Otherwise risk endless chicken loop
match = detect_screen_object(ScreenObjects.InGame, img)
if match.valid:
if detect_screen_object(ScreenObjects.InGame, img).valid:
health_percentage = meters.get_health(img)
mana_percentage = meters.get_mana(img)
# check rejuv
@@ -105,8 +103,7 @@ class HealthManager:
belt.drink_potion("mana", stats=[health_percentage, mana_percentage])
self._last_mana = time.time()
# check merc
match = detect_screen_object(ScreenObjects.MercIcon)
if match.valid:
if detect_screen_object(ScreenObjects.MercIcon, img).valid:
merc_health_percentage = meters.get_merc_health(img)
last_drink = time.time() - self._last_merc_heal
if merc_health_percentage < Config().char["merc_chicken"]:
@@ -118,6 +115,9 @@ class HealthManager:
elif merc_health_percentage < Config().char["heal_merc"] and last_drink > 7.0:
belt.drink_potion("health", merc=True, stats=[merc_health_percentage])
self._last_merc_heal = time.time()
if detect_screen_object(ScreenObjects.QuestSkillBtn, img).valid:
Logger.warning(f"Skill, stats, or quest button appeared--quit to avoid accidental allocations.")
self._do_chicken(img)
Logger.debug("Stop health monitoring")

View File

@@ -144,6 +144,7 @@ def transfer_items(items: list, action: str = "drop") -> list:
wait(0.2, 0.4)
for item in filtered:
attempts = 0
prev_gold_img = cut_roi(grab(), roi=Config().ui_roi["inventory_gold_digits"])
while attempts < 2:
# move to item position and left click
mouse.move(*item.pos, randomize=4, delay_factor=[0.2, 0.4])
@@ -159,15 +160,23 @@ def transfer_items(items: list, action: str = "drop") -> list:
mouse.press(button="left")
wait(0.2, 0.3)
mouse.release(button="left")
wait(0.2, 0.3)
wait(0.8, 1)
# check if item is still there
slot_img = get_slot_pos_and_img(grab(), item.column, item.row)[1]
img=grab()
slot_img = get_slot_pos_and_img(img, item.column, item.row)[1]
if not slot_has_item(slot_img):
# item successfully transferred, delete from list
for cnt, o_item in enumerate(items):
if o_item.pos == item.pos:
items.pop(cnt)
break
# check and see if inventory gold count changed
new_gold_img = cut_roi(img, roi=Config().ui_roi["inventory_gold_digits"])
if prev_gold_img.shape == new_gold_img.shape and not(np.bitwise_xor(prev_gold_img, new_gold_img).any()):
Logger.info("Inventory gold is full, force stash")
personal.set_inventory_gold_full(True)
else:
personal.set_inventory_gold_full(False)
break
else:
# item is still there, try again

View File

@@ -19,10 +19,19 @@ from ui_manager import detect_screen_object, wait_for_screen_object, ScreenObjec
from item import ItemCropper
from messages import Messenger
inv_gold_full = False
messenger = Messenger()
item_finder = ItemFinder()
nontradable_items = ["key of ", "essense of", "wirt's", "jade figurine"]
def get_inventory_gold_full():
global inv_gold_full
return inv_gold_full
def set_inventory_gold_full(bool):
global inv_gold_full
inv_gold_full = bool
def inventory_has_items(img: np.ndarray = None, close_window = False) -> bool:
"""
Check if Inventory has any items
@@ -61,7 +70,7 @@ def stash_all_items(items: list = None):
if Config().char["stash_gold"]:
img = grab()
if detect_screen_object(ScreenObjects.GoldNone, img).valid:
Logger.debug("Skipping gold stashing")
Logger.debug("No gold to stash")
else:
Logger.debug("Stashing gold")
stash.move_to_stash_tab(min(3, stash.curr_stash["gold"]))
@@ -69,6 +78,8 @@ def stash_all_items(items: list = None):
# Try to read gold count with OCR
stash_full_of_gold = common.read_gold(img, "stash") == 2500000
except:
stash_full_of_gold = False
if not stash_full_of_gold:
# If gold read by OCR fails, fallback to old method
mouse.move(*gold_btn.center, randomize=4)
wait(0.1, 0.15)
@@ -93,6 +104,8 @@ def stash_all_items(items: list = None):
# move to next stash
wait(0.5, 0.6)
return stash_all_items(items=items)
else:
set_inventory_gold_full(False)
# check if stash tab is completely full (no empty slots)
stash.move_to_stash_tab(stash.curr_stash["items"])
while stash.curr_stash["items"] <= 3:

View File

@@ -5,16 +5,17 @@ import os
from utils.misc import wait
from utils.custom_mouse import mouse
from logger import Logger
from messages import Messenger
curr_stash = {
"items": 3 if Config().char["fill_shared_stash_first"] else 0,
"gold": 0
}
def stash_full(self):
def stash_full():
Logger.error("All stash is full, quitting")
if self._config.general["custom_message_hook"]:
self._messenger.send_stash()
if Config().general["custom_message_hook"]:
Messenger().send_stash()
os.system("taskkill /f /im D2R.exe")
wait(1.0, 1.5)
os._exit(0)

View File

@@ -4,11 +4,11 @@ from template_finder import TemplateFinder
from config import Config
import numpy as np
from utils.misc import wait
from screen import convert_screen_to_monitor, grab
from screen import grab
from logger import Logger
from utils.custom_mouse import mouse
from ui_manager import detect_screen_object, wait_for_screen_object, ScreenObjects
from inventory import personal, stash, common
from inventory import personal, common
gamble_count = 0
gamble_status = False
@@ -92,6 +92,7 @@ def gamble():
if new_count >= max_gamble_count:
break
Logger.debug(f"Finish gambling")
personal.set_inventory_gold_full(False)
set_gamble_status(False)
common.close()
return None
@@ -122,6 +123,7 @@ def buy_item(template_name: str, quantity: int = 1, img: np.ndarray = None, shif
keyboard.send("esc")
return False
keyboard.send('shift', do_release=True)
personal.set_inventory_gold_full(False)
return True
if quantity:
for _ in range(quantity):
@@ -131,6 +133,7 @@ def buy_item(template_name: str, quantity: int = 1, img: np.ndarray = None, shif
Logger.warning(f"Out of gold, could not purchase {template_name}")
keyboard.send("esc")
return False
personal.set_inventory_gold_full(False)
return True
else:
Logger.error("buy_item: Quantity not specified")

View File

@@ -112,7 +112,7 @@ class ItemCropper:
x, y, w, h = cv2.boundingRect(cntr)
cropped_item = inp_img[y:y+h, x:x+w]
avg = np.average(cv2.cvtColor(cropped_item, cv2.COLOR_BGR2GRAY))
mostly_dark = True if 0 < avg < 20 else False
mostly_dark = True if 0 < avg < 25 else False
contains_black = True if np.min(cropped_item) < 14 else False
contains_white = True if np.max(cropped_item) > 250 else False
contains_orange = False

View File

@@ -2,13 +2,13 @@ import logging
import io
import os
import sys
import re
import warnings
from version import __version__
class Logger:
"""Manage logging"""
_logger_level = None
_formatter = logging.Formatter('[%(asctime)s] %(levelname)-10s %(message)s')
_formatter = logging.Formatter(f'[{__version__} %(asctime)s] %(levelname)-10s %(message)s')
_log_contents = io.StringIO()
_current_log_file_path = "info.log"
_output = "" # intercepted output from stdout and stderr
@@ -22,7 +22,7 @@ class Logger:
if Logger.logger is None:
Logger.init()
Logger.logger.debug(data)
@staticmethod
def info(data: str):
if Logger.logger is None:
@@ -82,7 +82,7 @@ class Logger:
Logger.logger.addHandler(Logger.string_handler)
Logger.logger.addHandler(Logger.console_handler)
Logger.logger.addHandler(Logger.file_handler)
# redirect stderr & stdout to logger, e.g. print("...")
# would have to implement all the std func such as write() flush() etc.
# sys.stderr = Logger

View File

@@ -48,7 +48,6 @@ def main():
GameController(),
GraphicDebuggerController()
)
screen.start_detecting_window()
if Config().advanced_options["logg_lvl"] == "info":
Logger.init(logging.INFO)
elif Config().advanced_options["logg_lvl"] == "debug":

View File

@@ -19,16 +19,20 @@ FIND_WINDOW = WindowSpec(
process_name_regex=Config().advanced_options["hwnd_window_process"],
)
def get_offset_state():
global found_offsets
return found_offsets
def start_detecting_window():
global detect_window, detect_window_thread
detect_window = True
if detect_window_thread is None:
Logger.debug(f"Using WinAPI to search for window: {FIND_WINDOW}")
detect_window_thread = threading.Thread(target=detect_window_position)
detect_window_thread.start()
def detect_window_position():
global detect_window
Logger.debug('Detect window thread started')
while detect_window:
find_and_set_window_position()
Logger.debug('Detect window thread stopped')
@@ -38,7 +42,7 @@ def find_and_set_window_position():
).advanced_options["window_client_area_offset"])
if position is not None:
set_window_position(*position)
wait(0.5)
wait(1)
def set_window_position(offset_x: int, offset_y: int):
global monitor_roi, monitor_x_range, monitor_y_range, found_offsets
@@ -105,5 +109,3 @@ def convert_abs_to_monitor(abs_coord: Tuple[float, float]) -> Tuple[float, float
screen_coord = convert_abs_to_screen(abs_coord)
monitor_coord = convert_screen_to_monitor(screen_coord)
return monitor_coord
Logger.debug(f"Using WinAPI to search for window: {FIND_WINDOW}")

View File

@@ -32,11 +32,11 @@ class A1(IAct):
return False
def open_wp(self, curr_loc: Location) -> bool:
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_SOUTH), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_SOUTH), self._char, force_move=True): return False
wait(0.5, 0.7)
if not TemplateFinder().search("A1_WP", grab()).valid:
curr_loc = Location.A1_WP_SOUTH
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_NORTH), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A1_WP_NORTH), self._char, force_move=True): return False
wait(0.5, 0.7)
found_wp_func = lambda: detect_screen_object(ScreenObjects.WaypointLabel).valid
# decreased threshold because we sometimes walk "over" it during pathing
@@ -50,7 +50,7 @@ class A1(IAct):
return False
def identify(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A1_KASHYA_CAIN), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A1_KASHYA_CAIN), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")
return Location.A1_KASHYA_CAIN
@@ -63,7 +63,7 @@ class A1(IAct):
return Location.A1_AKARA
def open_stash(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A1_STASH), self._char):
if not self._pather.traverse_nodes((curr_loc, Location.A1_STASH), self._char, force_move=True):
return False
wait(0.5, 0.6)
def stash_is_open_func():
@@ -81,7 +81,7 @@ class A1(IAct):
return Location.A1_AKARA
def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A1_CHARSI), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A1_CHARSI), self._char, force_move=True): return False
open_npc_menu(Npc.CHARSI)
press_npc_btn(Npc.CHARSI, "trade_repair")
return Location.A1_CHARSI

View File

@@ -61,7 +61,7 @@ class A3(IAct):
return False
def identify(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A3_STASH_WP), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A3_STASH_WP), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")
return Location.A3_STASH_WP

View File

@@ -33,7 +33,7 @@ class A4(IAct):
return False
def open_wp(self, curr_loc: Location) -> bool:
if not self._pather.traverse_nodes((curr_loc, Location.A4_WP), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A4_WP), self._char, force_move=True): return False
wait(0.5, 0.7)
found_wp_func = lambda: detect_screen_object(ScreenObjects.WaypointLabel).valid
# decreased threshold because we sometimes walk "over" it during pathing
@@ -86,7 +86,7 @@ class A4(IAct):
return False
def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A4_HALBU), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A4_HALBU), self._char, force_move=True): return False
if open_npc_menu(Npc.HALBU):
press_npc_btn(Npc.HALBU, "trade_repair")
return Location.A4_HALBU

View File

@@ -37,21 +37,21 @@ class A5(IAct):
return False
def resurrect(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A5_QUAL_KEHK), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A5_QUAL_KEHK), self._char, force_move=True): return False
if open_npc_menu(Npc.QUAL_KEHK):
press_npc_btn(Npc.QUAL_KEHK, "resurrect")
return Location.A5_QUAL_KEHK
return False
def identify(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A5_QUAL_KEHK), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A5_QUAL_KEHK), self._char, force_move=True): return False
if open_npc_menu(Npc.CAIN):
press_npc_btn(Npc.CAIN, "identify")
return Location.A5_QUAL_KEHK
return False
def open_stash(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A5_STASH), self._char):
if not self._pather.traverse_nodes((curr_loc, Location.A5_STASH), self._char, force_move=True):
return False
wait(0.5, 0.6)
def stash_is_open_func():
@@ -64,13 +64,13 @@ class A5(IAct):
return Location.A5_STASH
def open_trade_and_repair_menu(self, curr_loc: Location) -> Union[Location, bool]:
if not self._pather.traverse_nodes((curr_loc, Location.A5_LARZUK), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A5_LARZUK), self._char, force_move=True): return False
open_npc_menu(Npc.LARZUK)
press_npc_btn(Npc.LARZUK, "trade_repair")
return Location.A5_LARZUK
def open_wp(self, curr_loc: Location) -> bool:
if not self._pather.traverse_nodes((curr_loc, Location.A5_WP), self._char): return False
if not self._pather.traverse_nodes((curr_loc, Location.A5_WP), self._char, force_move=True): return False
wait(0.5, 0.7)
found_wp_func = lambda: detect_screen_object(ScreenObjects.WaypointLabel).valid
return self._char.select_by_template("A5_WP", found_wp_func, telekinesis=True)

View File

@@ -5,6 +5,7 @@ from pather import Location
from logger import Logger
from town import IAct, A1, A2, A3, A4, A5
from utils.misc import wait
from screen import grab
from ui import waypoint, view
from inventory import consumables, personal, vendor, common
@@ -97,41 +98,42 @@ class TownManager:
if self._acts[curr_act].can_buy_pots():
new_loc = self._acts[curr_act].open_trade_menu(curr_loc)
if not new_loc: return False, items
img=grab()
# Buy HP pots
if consumables.get_needs("health") > 0:
can_shift_click = not sum([ x > 0 for x in [consumables.get_needs("health"), consumables.get_needs("mana"), consumables.get_needs("rejuv")]]) > 1
if vendor.buy_item(template_name = "SUPER_HEALING_POTION", quantity = consumables.get_needs("health"), shift_click = can_shift_click):
if vendor.buy_item(template_name = "SUPER_HEALING_POTION", quantity = consumables.get_needs("health"), shift_click = can_shift_click, img=img):
consumables.set_needs("health", 0)
else:
if vendor.buy_item(template_name="GREATER_HEALING_POTION", quantity = consumables.get_needs("health"), shift_click = can_shift_click):
if vendor.buy_item(template_name="GREATER_HEALING_POTION", quantity = consumables.get_needs("health"), shift_click = can_shift_click, img=img):
consumables.set_needs("health", 0)
else:
Logger.error("buy_consumables: Error purchasing health potions")
# Buy mana pots
if consumables.get_needs("mana") > 0:
can_shift_click = not sum([ x > 0 for x in [consumables.get_needs("health"), consumables.get_needs("mana"), consumables.get_needs("rejuv")]]) > 1
if vendor.buy_item(template_name="SUPER_MANA_POTION", quantity = consumables.get_needs("mana"), shift_click = can_shift_click):
if vendor.buy_item(template_name="SUPER_MANA_POTION", quantity = consumables.get_needs("mana"), shift_click = can_shift_click, img=img):
consumables.set_needs("mana", 0)
else:
if vendor.buy_item(template_name="GREATER_MANA_POTION", quantity = consumables.get_needs("mana"), shift_click = can_shift_click):
if vendor.buy_item(template_name="GREATER_MANA_POTION", quantity = consumables.get_needs("mana"), shift_click = can_shift_click, img=img):
consumables.set_needs("mana", 0)
else:
Logger.error("buy_consumables: Error purchasing mana potions")
# Buy TP scrolls
if consumables.get_needs("tp") > 0:
if vendor.buy_item(template_name="INV_SCROLL_TP", shift_click = True):
if vendor.buy_item(template_name="INV_SCROLL_TP", shift_click = True, img=img):
consumables.set_needs("tp", 0)
else:
Logger.error("buy_consumables: Error purchasing teleport scrolls")
# Buy ID scrolls
if consumables.get_needs("id") > 0:
if vendor.buy_item(template_name="INV_SCROLL_ID", shift_click = True):
if vendor.buy_item(template_name="INV_SCROLL_ID", shift_click = True, img=img):
consumables.set_needs("id", 0)
else:
Logger.error("buy_consumables: Error purchasing ID scrolls")
# Buy keys
if consumables.get_needs("key") > 0:
if vendor.buy_item(template_name="INV_KEY", shift_click = True):
if vendor.buy_item(template_name="INV_KEY", shift_click = True, img=img):
consumables.set_needs("key", 0)
else:
Logger.error("buy_consumables: Error purchasing keys")

View File

@@ -262,6 +262,12 @@ class ScreenObjects:
color_match=Config().colors["red"],
use_grayscale=True
)
QuestSkillBtn=ScreenObject(
ref="QUEST_SKILL_BTN",
threshold=0.9,
use_grayscale=True,
roi="quest_skill_btn"
)
def detect_screen_object(screen_object: ScreenObject, img: np.ndarray = None) -> TemplateMatch:
roi = Config().ui_roi[screen_object.roi] if screen_object.roi else None

View File

@@ -4,7 +4,7 @@ import keyboard
from bot import Bot
from template_finder import TemplateFinder
from utils.misc import wait, set_d2r_always_on_top
from screen import found_offsets, grab
from screen import get_offset_state, grab
from config import Config
@@ -39,7 +39,7 @@ def restart_game(d2_path = None):
attempts = 0
set_d2r_always_on_top()
while not success:
success = found_offsets
success = get_offset_state()
wait(0.5, 1.0)

View File

@@ -1 +1 @@
__version__ = '0.7.0-dev'
__version__ = '0.7.1'