From c358282e68b60997fbf37e31b2d84d35b9a15d91 Mon Sep 17 00:00:00 2001 From: Vladimir Makaev Date: Sat, 5 Feb 2022 01:27:56 +0000 Subject: [PATCH] Launch botty from town or main menu; fix character recovery after restart/death (#494) * Add ability to launch botty from either main menu or town * Fix game recovery routine is stuck forever on character selection screen * Move _char_template to class variable to be able to share among instances * fix character recovery and character select, support offline/online. WIP Co-authored-by: Vladimir Makayev Co-authored-by: mgleed --- assets/templates/character_active.png | Bin 0 -> 13441 bytes assets/templates/character_state_offline.png | Bin 0 -> 10669 bytes assets/templates/character_state_online.png | Bin 0 -> 10692 bytes config/game.ini | 13 +- src/bot.py | 68 ++++++++--- src/game_controller.py | 16 +-- src/game_recovery.py | 20 +-- src/town/town_manager.py | 16 +-- src/ui/__init__.py | 1 + src/ui/char_selector.py | 121 ++++++++++++++----- src/utils/misc.py | 2 +- 11 files changed, 168 insertions(+), 89 deletions(-) create mode 100644 assets/templates/character_active.png create mode 100644 assets/templates/character_state_offline.png create mode 100644 assets/templates/character_state_online.png diff --git a/assets/templates/character_active.png b/assets/templates/character_active.png new file mode 100644 index 0000000000000000000000000000000000000000..6789e90635d47352ed0d06a79c6b8eec88ebd50e GIT binary patch literal 13441 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 ZT^gvvInp%V^T(frP#XH`m8w?Z{|jn!B0)f; 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 BeltManager: @@ -158,7 +164,7 @@ class Bot: return self._curr_loc def start(self): - self.trigger('create_game') + self.trigger_or_stop('init') def stop(self): self._stopping = True @@ -196,14 +202,42 @@ class Bot: break return not found_unfinished_run - def on_create_game(self): - keyboard.release(self._config.char["stand_still"]) - # Start a game from hero selection - self._game_stats.log_start_game() - self._template_finder.search_and_wait(["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"], roi=self._config.ui_roi["main_menu_top_left"]) - if not self._ui_manager.start_game(): return - self._curr_loc = self._town_manager.wait_for_town_spawn() + def _rebuild_as_asset_to_trigger(trigger_to_assets: dict): + result = {} + for key in trigger_to_assets.keys(): + for asset in trigger_to_assets[key]: + result[asset] = key + return result + def on_init(self): + keyboard.release(self._config.char["stand_still"]) + transition_to_screens = Bot._rebuild_as_asset_to_trigger({ + "select_character": Bot._MAIN_MENU_MARKERS, + "start_from_town": town_manager.TOWN_MARKERS, + }) + match = self._template_finder.search_and_wait(list(transition_to_screens.keys()), best_match=True) + self.trigger_or_stop(transition_to_screens[match.name]) + + def on_select_character(self): + if self._config.general['restart_d2r_when_stuck']: + # Make sure the correct char is selected + if self._char_selector.has_char_template_saved(): + self._char_selector.select_char() + else: + self._char_selector.save_char_online_status() + self._char_selector.save_char_template() + + self.trigger_or_stop("create_game") + + def on_create_game(self): + # Start a game from hero selection + self._template_finder.search_and_wait(Bot._MAIN_MENU_MARKERS, roi=self._config.ui_roi["main_menu_top_left"]) + if not self._ui_manager.start_game(): return + self.trigger_or_stop("start_from_town") + + def on_start_from_town(self): + self._game_stats.log_start_game() + self._curr_loc = self._town_manager.wait_for_town_spawn() # Check for the current game ip and pause if we are able to obtain the hot ip if self._config.dclone["region_ips"] != "" and self._config.dclone["dclone_hotip"] != "": cur_game_ip = get_d2r_game_ip() @@ -349,7 +383,7 @@ class Bot: if self._config.general["randomize_runs"]: self.shuffle_runs() wait(0.2, 0.5) - self.trigger_or_stop("create_game") + self.trigger_or_stop("init") def on_end_run(self): if not self._config.char["pre_buff_every_run"]: diff --git a/src/game_controller.py b/src/game_controller.py index 6886b08..317c22e 100644 --- a/src/game_controller.py +++ b/src/game_controller.py @@ -14,7 +14,6 @@ from health_manager import HealthManager from logger import Logger from messages import Messenger from screen import Screen -from ui.char_selector import CharSelector from utils.restart import restart_game from utils.misc import kill_thread, set_d2r_always_on_top, restore_d2r_window_visibility @@ -34,17 +33,8 @@ class GameController: self.game_controller_thread = None self.bot_thread = None self.bot = None - self.char_selector = None - def run_bot(self, pick_corpse: bool = False): - if self._config.general['restart_d2r_when_stuck']: - # Make sure the correct char is selected - if self.char_selector.has_char_template_saved(): - Logger.info("Selecting original char") - self.char_selector.select_char() - else: - Logger.info("Saving top-most char as template") - self.char_selector.save_char_template() + def run_bot(self, pick_corpse: bool = False): # Start bot thread self.bot = Bot(self.screen, self.game_stats, self.template_finder, pick_corpse) self.bot_thread = threading.Thread(target=self.bot.start) @@ -100,6 +90,7 @@ class GameController: if restart_game(self._config.general["d2r_path"]): self.game_stats.log_end_game(failed=max_game_length_reached) if self.setup_screen(): + self.template_finder = TemplateFinder(self.screen) self.start_health_manager_thread() self.start_death_manager_thread() self.game_recovery = GameRecovery(self.screen, self.death_manager, self.template_finder) @@ -124,8 +115,7 @@ class GameController: self.start_health_manager_thread() self.start_death_manager_thread() self.game_recovery = GameRecovery(self.screen, self.death_manager, self.template_finder) - self.game_stats = GameStats() - self.char_selector = CharSelector(self.screen, self.template_finder) + self.game_stats = GameStats() self.start_game_controller_thread() self.is_running = True diff --git a/src/game_recovery.py b/src/game_recovery.py index 0ec9cf4..389c216 100644 --- a/src/game_recovery.py +++ b/src/game_recovery.py @@ -35,19 +35,6 @@ class GameRecovery: # lets just see if you might already be at hero selection found = self._template_finder.search(["MAIN_MENU_TOP_LEFT","MAIN_MENU_TOP_LEFT_DARK"], self._screen.grab(), roi=self._config.ui_roi["main_menu_top_left"]).valid if found: - offline_tab = self._template_finder.search(["OFFLINE_TAB","OFFLINE_TAB_DARK"], self._screen.grab(), roi=self._config.ui_roi["offline_tab"], threshold=0.8, best_match = True) - if offline_tab.valid: - if offline_tab.name == "OFFLINE_TAB_DARK": - #We need to press escape before clicking the tab - keyboard.send("esc") - time.sleep(0.2) - # Right now it selects the tab to the left of offline - offline_width = self._config.ui_pos["offline_width"] - tab = (offline_tab.center[0] - offline_width , offline_tab.center[1]) - x, y = self._screen.convert_screen_to_monitor(tab) - mouse.move(x, y, randomize=8) - time.sleep(0.5) - mouse.click(button="left") return True # would have been too easy, maybe we have died? if self._death_manager.handle_death_screen(): @@ -70,6 +57,7 @@ if __name__ == "__main__": keyboard.add_hotkey('f12', lambda: os._exit(1)) keyboard.wait("f11") screen = Screen() - death_manager = DeathManager(screen) - game_recovery = GameRecovery(screen, death_manager) - game_recovery.go_to_hero_selection() + template_finder = TemplateFinder(screen) + death_manager = DeathManager(screen, template_finder) + game_recovery = GameRecovery(screen, death_manager, template_finder) + print(game_recovery.go_to_hero_selection()) diff --git a/src/town/town_manager.py b/src/town/town_manager.py index f295542..db7cad4 100644 --- a/src/town/town_manager.py +++ b/src/town/town_manager.py @@ -9,8 +9,16 @@ from ui import UiManager from town import IAct, A1, A2, A3, A4, A5 from utils.misc import wait +TOWN_MARKERS = [ + "A5_TOWN_0", "A5_TOWN_1", + "A4_TOWN_4", "A4_TOWN_5", + "A3_TOWN_0", "A3_TOWN_1", + "A2_TOWN_0", "A2_TOWN_1", "A2_TOWN_10", + "A1_TOWN_1", "A1_TOWN_3" + ] class TownManager: + def __init__(self, template_finder: TemplateFinder, ui_manager: UiManager, item_finder: ItemFinder, a1: A1, a2: A2, a3: A3, a4: A4, a5: A5): self._config = Config() self._template_finder = template_finder @@ -44,13 +52,7 @@ class TownManager: :param time_out: Optional float value for time out in seconds, defaults to None :return: Location of the town (e.g. Location.A4_TOWN_START) or None if nothing was found within time_out time """ - template_match = self._template_finder.search_and_wait([ - "A5_TOWN_0", "A5_TOWN_1", - "A4_TOWN_4", "A4_TOWN_5", - "A3_TOWN_0", "A3_TOWN_1", - "A2_TOWN_0", "A2_TOWN_1", "A2_TOWN_10", - "A1_TOWN_1", "A1_TOWN_3" - ], best_match=True, time_out=time_out) + template_match = self._template_finder.search_and_wait(TOWN_MARKERS, best_match=True, time_out=time_out) if template_match.valid: return TownManager.get_act_from_location(template_match.name) return None diff --git a/src/ui/__init__.py b/src/ui/__init__.py index 761560d..3544a81 100644 --- a/src/ui/__init__.py +++ b/src/ui/__init__.py @@ -1,2 +1,3 @@ from .ui_manager import UiManager from .belt_manager import BeltManager +from .char_selector import CharSelector diff --git a/src/ui/char_selector.py b/src/ui/char_selector.py index 29dbf5e..636695c 100644 --- a/src/ui/char_selector.py +++ b/src/ui/char_selector.py @@ -1,52 +1,114 @@ +from operator import is_ +import cv2 +import numpy as np +import time + from config import Config from utils.custom_mouse import mouse -from utils.misc import cut_roi, wait +from utils.misc import cut_roi, roi_center, wait, is_in_roi from screen import Screen from template_finder import TemplateFinder - +from logger import Logger class CharSelector: + _last_char_template = None + _online_character = None + def __init__(self, screen: Screen, template_finder: TemplateFinder): self._config = Config() - self._char_template = None self._screen = screen self._template_finder = template_finder def has_char_template_saved(self): - return self._char_template is not None + return CharSelector._last_char_template is not None def save_char_template(self): img = self._screen.grab() - # TODO: check which character is currently selected rather than assuming the top one is the correct one - self._char_template = cut_roi(img, self._config.ui_roi["char_selection_top"]) + active_character = self._template_finder.search("CHARACTER_ACTIVE", img, roi = self._config.ui_roi["character_select"], threshold = 0.8) + if active_character.valid: + x, y, w, h = self._config.ui_roi["character_name_sub_roi"] + x, y = x + active_character.region[0], y + active_character.region[1] + char_template = cut_roi(img, [x, y, w, h]) + if self._config.general["info_screenshots"]: + cv2.imwrite("./info_screenshots/saved_character_" + time.strftime("%Y%m%d_%H%M%S") + ".png", char_template) + Logger.debug(f"Saved character template") + else: + Logger.error("save_char_template: Could not save character template") + return + CharSelector._last_char_template = char_template + + def save_char_online_status(self): + online_tabs = self._template_finder.search(["CHARACTER_STATE_OFFLINE","CHARACTER_STATE_ONLINE"], self._screen.grab(), roi=self._config.ui_roi["character_online_status"], threshold=0.8, best_match = True) + if online_tabs.valid: + if online_tabs.name == "CHARACTER_STATE_ONLINE": + online_status = True + else: + online_status = False + Logger.debug(f"Saved online status. Online={online_status}") + else: + Logger.error("save_char_online_status: Could not determine character's online status") + return + CharSelector._online_character = online_status + + def select_online_tab(self, region: tuple[int, int, int, int] = None, center: tuple[int, int] = None): + btn_width = center[0] - region[0] + if CharSelector._online_character: + Logger.debug(f"Selecting online tab") + x = region[0] + (btn_width / 2) + else: + Logger.debug(f"Selecting offline tab") + x = region[0] + (3 * btn_width / 2) + pos = self._screen.convert_screen_to_monitor((x, center[1])) + # move cursor to result and select + mouse.move(*pos) + wait(0.4, 0.6) + mouse.click(button="left") + wait(0.4, 0.6) def select_char(self): - if self._char_template is not None: + if CharSelector._last_char_template is not None: + online_tabs = self._template_finder.search(["CHARACTER_STATE_OFFLINE","CHARACTER_STATE_ONLINE"], self._screen.grab(), roi=self._config.ui_roi["character_online_status"], threshold=0.8, best_match = True) + if online_tabs.valid: + if online_tabs.name == "CHARACTER_STATE_ONLINE" and (not CharSelector._online_character): + self.select_online_tab(online_tabs.region, online_tabs.center) + elif online_tabs.name == "CHARACTER_STATE_OFFLINE" and (CharSelector._online_character): + self.select_online_tab(online_tabs.region, online_tabs.center) + wait(1, 1.5) + else: + Logger.error("select_char: Could not find online/offline tabs") + return + img = self._screen.grab() + active_char = self._template_finder.search("CHARACTER_ACTIVE", img, roi = self._config.ui_roi["character_select"], threshold = 0.8, normalize_monitor = True) + if not active_char.valid: + Logger.error("select_char: Could not find highlighted profile") + return scrolls_attempts = 0 - while scrolls_attempts < 2: # 2 scrolls should suffice to see all possible characters - template_result = self._template_finder.search( - self._char_template, - self._screen.grab(), - threshold=0.8, - roi=self._config.ui_roi["char_selection_all"], - normalize_monitor=True - ) - if template_result.valid: - # move cursor to result and select - mouse.move(*template_result.center) - wait(0.5) - mouse.click(button="left") - break + while scrolls_attempts < 2: + if scrolls_attempts > 0: + img = self._screen.grab() + desired_char = self._template_finder.search(CharSelector._last_char_template, img, roi = self._config.ui_roi["character_select"], threshold = 0.8, normalize_monitor = True) + if desired_char.valid: + if is_in_roi(active_char.region, desired_char.center) and scrolls_attempts == 0: + Logger.debug("Saved character template found and already highlighted, continue") + return + else: + Logger.debug("Selecting saved character") + mouse.move(*desired_char.center) + wait(0.4, 0.6) + mouse.click(button="left") + wait(0.4), 0.6 + return else: + Logger.debug("Highlighted profile found but saved character not in view, scroll") # We can scroll the characters only if we have the mouse in the char names selection so move the mouse there - pos = (self._config.ui_pos["char_selection_center_x"], self._config.ui_pos["char_selection_center_y"]) - pos = self._screen.convert_screen_to_monitor(pos) - mouse.move(*pos) - wait(0.5) + center = roi_center(self._config.ui_roi["character_select"]) + center = self._screen.convert_screen_to_monitor(center) + mouse.move(*center) + wait(0.4, 0.6) mouse.wheel(-14) scrolls_attempts += 1 - wait(0.5) - return False + wait(0.4, 0.6) + Logger.error(f"select_char: unable to find saved profile after {scrolls_attempts} scroll attempts") if __name__ == "__main__": @@ -59,4 +121,9 @@ if __name__ == "__main__": selector = CharSelector(screen, tf) if not selector.has_char_template_saved(): selector.save_char_template() + print("Move D2R window, select another character and press F11 to continue") + keyboard.wait("f11") + screen = Screen() + tf = TemplateFinder(screen) + selector = CharSelector(screen,tf) selector.select_char() diff --git a/src/utils/misc.py b/src/utils/misc.py index 30f7b8d..f462e64 100644 --- a/src/utils/misc.py +++ b/src/utils/misc.py @@ -112,7 +112,7 @@ def erode_to_black(img: np.ndarray, threshold: int = 14): img = cv2.bitwise_and(img, mask_color_r) return img -def roi_center(roi: list[float] = None, scale: float = 1): +def roi_center(roi: list[float] = None): x, y, w, h = roi return round(x + w/2), round(y + h/2)