From db994eeee463766ea3d16e5ee2c8503d0afa1334 Mon Sep 17 00:00:00 2001 From: aeon0 Date: Wed, 17 Nov 2021 22:49:38 +0100 Subject: [PATCH] Recover from being stuck (#46) * recover from being stuck * remove shutdown within bot.py --- README.md | 1 + params.ini | 1 + src/bot.py | 75 ++++++++++++++++++++++++-------------------- src/config.py | 1 + src/game_recovery.py | 38 ++++++++++++++++++++++ src/run.py | 40 +++++++++++++++++------ 6 files changed, 112 insertions(+), 44 deletions(-) create mode 100644 src/game_recovery.py diff --git a/README.md b/README.md index b2afbc4..3df3fcc 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ res | Resolution settings can be any of [1920_1080, 1280_720] offset_top | Your D2R windows offset from top of the screen (including the window bar). For fullscreen leave at 0. offset_left | Your D2R window offset from left of screen. For fullscreen leave at 0. min_game_length_s | Games must have at least this length, will wait in hero selection for if game is too quick (to avoid server connection issues) +max_game_length_s | Botty will attempt to stop whatever its doing and try to restart a new game. Note if this fails, botty will attempt to shut down D2R and Bnet exit_key | Pressing this key (anywhere), will force botty to shut down resume_key | After starting the exe botty will wait for this keypress to atually start botting away color_checker_key | Pressing this key will start a debug mode to check if the color filtering works with your settings. It also includes the item search and marks items it would pick up with red circles diff --git a/params.ini b/params.ini index 209e9e4..147474b 100644 --- a/params.ini +++ b/params.ini @@ -4,6 +4,7 @@ res=1920_1080 offset_top=0 offset_left=0 min_game_length_s=65 +max_game_length_s=320 resume_key=f11 exit_key=f12 auto_settings_key=f9 diff --git a/src/bot.py b/src/bot.py index 0dc63f5..e5de0fd 100644 --- a/src/bot.py +++ b/src/bot.py @@ -14,7 +14,7 @@ from health_manager import HealthManager from death_manager import DeathManager from npc_manager import NpcManager, Npc from pickit import PickIt -from utils.misc import wait, send_discord, close_down_d2 +from utils.misc import kill_thread, wait import keyboard import threading import time @@ -57,6 +57,9 @@ class Bot: self._timer = None self._tps_left = 20 self._pre_buffed = 0 + self._stopping = False + self._pausing = False + self._current_threads = [] self._states=['hero_selection', 'a5_town', 'pindle', 'shenk'] self._transitions = [ @@ -81,6 +84,23 @@ class Bot: def start(self): self.trigger('create_game') + def stop(self): + self._stopping = True + for t in self._current_threads: + kill_thread(t) + + def toggle_pause(self): + self._pausing = not self._pausing + Logger.info(f"Pause at next state change...") if self._pausing else Logger.info(f"Resume") + + def trigger_or_stop(self, name: str): + if self._pausing: + Logger.info("Botty is now pausing") + while self._pausing: + time.sleep(0.2) + if not self._stopping: + self.trigger(name) + def current_game_length(self): if self._timer is None: return 0 @@ -99,19 +119,6 @@ class Bot: break return not found_unfinished_run - def _shut_down(self): - Logger.error("Something went wrong here, bot is unsure about current location. Closing down bot.") - if self._config.general["custom_discord_hook"] != "": - send_discord_thread = threading.Thread( - target=send_discord, - args=("Botty got stuck and can not resume", self._config.general["custom_discord_hook"]) - ) - send_discord_thread.daemon = True - send_discord_thread.start() - self._ui_manager.save_and_exit() - close_down_d2() - os._exit(1) - def on_create_game(self): if self._timer is not None: delay = self._config.general["min_game_length_s"] - (time.time() - self._timer) @@ -120,20 +127,16 @@ class Bot: wait(delay, delay + 5.0) Logger.info("Start new game") self._timer = time.time() - found, _ = self._template_finder.search_and_wait("D2_LOGO_HS", time_out=70) - if not found: - self._shut_down() + self._template_finder.search_and_wait("D2_LOGO_HS") self._ui_manager.start_game() - found, _ = self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"], time_out=50) - if not found: - self._shut_down() + self._template_finder.search_and_wait(["A5_TOWN_1", "A5_TOWN_0"]) self._tp_is_up = False self._curr_location = Location.A5_TOWN_START # Make sure these keys are released keyboard.release(self._config.char["stand_still"]) wait(0.05, 0.15) keyboard.release(self._config.char["show_items"]) - self.trigger("maintenance") + self.trigger_or_stop("maintenance") def on_maintenance(self): time.sleep(0.6) @@ -152,12 +155,12 @@ class Bot: if not self._tp_is_up and (self._health_manager.get_health(img) < 0.6 or self._health_manager.get_mana(img) < 0.3): Logger.info("Need some healing first. Go talk to Malah") if not self._pather.traverse_nodes(self._curr_location, Location.MALAH, self._char): - self.trigger("end_game") + self.trigger_or_stop("end_game") return self._curr_location = Location.MALAH self._npc_manager.open_npc_menu(Npc.MALAH) if not self._pather.traverse_nodes(self._curr_location, Location.A5_TOWN_START, self._char): - self.trigger("end_game") + self.trigger_or_stop("end_game") return self._curr_location = Location.A5_TOWN_START @@ -165,7 +168,7 @@ class Bot: if self._picked_up_items: Logger.info("Stashing picked up items") if not self._pather.traverse_nodes(self._curr_location, Location.A5_STASH, self._char): - self.trigger("end_game") + self.trigger_or_stop("end_game") return self._curr_location = Location.A5_STASH time.sleep(1.5) @@ -182,7 +185,7 @@ class Bot: if self._tps_left < 4: Logger.info("Repairing and buying tps at Lazurk") if not self._pather.traverse_nodes(self._curr_location, Location.LARZUK, self._char): - self.trigger("end_game") + self.trigger_or_stop("end_game") return self._curr_location = Location.LARZUK self._npc_manager.open_npc_menu(Npc.LARZUK) @@ -198,7 +201,7 @@ class Bot: if not merc_alive: Logger.info("Reviveing merc") if not self._pather.traverse_nodes(self._curr_location, Location.QUAL_KEHK, self._char): - self.trigger("end_game") + self.trigger_or_stop("end_game") return self._curr_location = Location.QUAL_KEHK if self._npc_manager.open_npc_menu(Npc.QUAL_KEHK): @@ -209,22 +212,25 @@ class Bot: started_run = False for key in self._do_runs: if self._do_runs[key]: - self.trigger(key) + self.trigger_or_stop(key) started_run = True break if not started_run: - self.trigger("end_game") + self.trigger_or_stop("end_game") def _start_run(self, key, run): Logger.info(f"{key}") self._do_runs[key] = False run_thread = threading.Thread(target=run.doit, args=(self,)) run_thread.start() + self._current_threads.append(run_thread) # Set up monitoring health_monitor_thread = threading.Thread(target=self._health_manager.start_monitor, args=(run_thread,)) health_monitor_thread.start() + self._current_threads.append(health_monitor_thread) death_monitor_thread = threading.Thread(target=self._death_manager.start_monitor, args=(run_thread,)) death_monitor_thread.start() + self._current_threads.append(death_monitor_thread) run_thread.join() # Run done, lets stop health monitoring and death monitoring self._health_manager.stop_monitor() @@ -236,10 +242,11 @@ class Bot: self._death_manager.stop_monitor() death_monitor_thread.join() + self._current_threads = [] if self._death_manager.died() or self._health_manager.did_chicken() or self.is_last_run() or not run.success: - self.trigger("end_game") + self.trigger_or_stop("end_game") else: - self.trigger("end_run") + self.trigger_or_stop("end_run") def on_run_pindle(self): class RunPindle: @@ -368,7 +375,7 @@ class Bot: if self._config.general["randomize_runs"]: self.shuffle_runs() wait(0.2, 0.5) - self.trigger("create_game") + self.trigger_or_stop("create_game") def on_end_run(self): success = self._char.tp_town() @@ -378,11 +385,11 @@ class Bot: if success: self._tp_is_up = True self._curr_location = Location.A5_TOWN_START - self.trigger("maintenance") + self.trigger_or_stop("maintenance") else: - self.trigger("end_game") + self.trigger_or_stop("end_game") else: - self.trigger("end_game") + self.trigger_or_stop("end_game") if __name__ == "__main__": diff --git a/src/config.py b/src/config.py index 29b3ad7..c941c35 100644 --- a/src/config.py +++ b/src/config.py @@ -30,6 +30,7 @@ class Config: "offset_top": int(self._select_val("general", "offset_top")), "offset_left": int(self._select_val("general", "offset_left")), "min_game_length_s": float(self._select_val("general", "min_game_length_s")), + "max_game_length_s": float(self._select_val("general", "max_game_length_s")), "exit_key": self._select_val("general", "exit_key"), "resume_key": self._select_val("general", "resume_key"), "auto_settings_key": self._select_val("general", "auto_settings_key"), diff --git a/src/game_recovery.py b/src/game_recovery.py new file mode 100644 index 0000000..1b66bc6 --- /dev/null +++ b/src/game_recovery.py @@ -0,0 +1,38 @@ +from death_manager import DeathManager +from screen import Screen +from template_finder import TemplateFinder +from config import Config +from death_manager import DeathManager +from ui_manager import UiManager +import keyboard +import time + + +class GameRecovery: + def __init__(self): + self._config = Config() + self._screen = Screen(self._config.general["monitor"]) + self._template_finder = TemplateFinder(self._screen) + self._death_manager = DeathManager(self._screen, self._template_finder) + self._ui_manager = UiManager(self._screen, self._template_finder) + + def go_to_hero_selection(self): + # first lets just see if you might already be at hero selection + found, _ = self._template_finder.search_and_wait("D2_LOGO_HS", time_out=1, take_ss=False) + if found: + return True + # would have been too easy, maybe we have died? + died = self._death_manager.handle_death_screen() + if died: + return self._ui_manager.save_and_exit() + # we must be ingame, but maybe we are at vendor or on stash, press "esc" until we find a save and exit btn (max 5 times) + for _ in range(5): + keyboard.press("esc") + time.sleep(1) + templates = ["SAVE_AND_EXIT_NO_HIGHLIGHT","SAVE_AND_EXIT_HIGHLIGHT"] + found, _ = self._template_finder.search_and_wait(templates, roi=self._config.ui_roi["save_and_exit"], time_out=1.5, take_ss=False) + if found: + keyboard.press("esc") + time.sleep(1) + return self._ui_manager.save_and_exit() + return False diff --git a/src/run.py b/src/run.py index 0e16007..c0bb36b 100644 --- a/src/run.py +++ b/src/run.py @@ -1,4 +1,5 @@ from bot import Bot +from game_recovery import GameRecovery from logger import Logger import keyboard import os @@ -6,17 +7,38 @@ from config import Config from utils.color_checker import run_color_checker from version import __version__ from utils.auto_settings import adjust_settings +from utils.misc import kill_thread, send_discord, close_down_d2 import threading from beautifultable import BeautifulTable import time import logging -def start_bot(bot): - try: - bot.start() - except KeyboardInterrupt: - Logger.info('Exit (ctrl+c)') or exit() +def run_bot(config: Config): + game_recovery = GameRecovery() + bot = Bot() + bot_thread = threading.Thread(target=bot.start) + bot_thread.start() + do_restart = False + keyboard.add_hotkey(config.general["exit_key"], lambda: Logger.info(f'Force Exit') or os._exit(1)) + keyboard.add_hotkey(config.general['resume_key'], lambda: bot.toggle_pause()) + while 1: + if bot.current_game_length() > config.general["max_game_length_s"]: + Logger.info("Max game length reached. Attempting to restart botty!") + bot.stop() + kill_thread(bot_thread) + if game_recovery.go_to_hero_selection(): + do_restart = False + break + time.sleep(0.04) + bot_thread.join() + if do_restart: + run_bot(config) + else: + Logger.error("Botty could not recover from a max game length violation. Shutting down everything.") + if config.general["custom_discord_hook"]: + send_discord("Botty got stuck and can not resume", config.general["custom_discord_hook"]) + close_down_d2() if __name__ == "__main__": @@ -36,7 +58,7 @@ if __name__ == "__main__": table = BeautifulTable() table.rows.append([config.general['auto_settings_key'], "Adjust D2R settings"]) table.rows.append([config.general['color_checker_key'], "Color test mode "]) - table.rows.append([config.general['resume_key'], "Start bot"]) + table.rows.append([config.general['resume_key'], "Start / Pause Botty"]) table.rows.append([config.general['exit_key'], "Stop bot"]) table.columns.header = ["hotkey", "action"] print(table) @@ -44,9 +66,8 @@ if __name__ == "__main__": while 1: if keyboard.is_pressed(config.general['resume_key']): - bot = Bot() - bot_thread = threading.Thread(target=start_bot, args=(bot,)) - bot_thread.start() + run_bot(config) + break if keyboard.is_pressed(config.general['auto_settings_key']): adjust_settings() elif keyboard.is_pressed(config.general['color_checker_key']): @@ -54,6 +75,5 @@ if __name__ == "__main__": break time.sleep(0.02) - bot_thread.join() print("Press Enter to exit ...") input()