diff --git a/README.md b/README.md index 466a2f5..538e507 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ And please. I urge you to actually read that README! It will make your life a lo [![Watch the video](assets/docs/video_thumbnail.png)](https://streamable.com/67h9ay) ## Getting started -Botty only supports English language and screen resolutions 1920x1080 and 1280x720. If your monitor has a higher resolution, you need to either reduce it to one of the two resolutions or set D2R to windowed mode and adjust offset_left and offset_top. +Botty only supports English language! Botty is currently working in 1080p or 720p and will try to adjust the D2R settings accordingly depending on your monitor res and your botty setting for "res" in the [general] section. ### 1) Graphics and Gameplay Settings All settings will automatically be set when you execute `run.exe` and press the hotkey for "Adjust D2R settings" (default f9). Note that D2R should not run during this process, or if it does you will have to restart afterwards. It is not a 100% thing, in rare cases you might still have to fiddle around with your brightness. I suggest using the "Graphic Debugger" to verify your settings. Also, there are sample screenshots of how graphics should look like: Example 1, Example 2
@@ -54,8 +54,6 @@ run_shenk=0 name | Name used in terminal and discord messages monitor | Select on which monitor D2R is running in case multiple are available 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. 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 diff --git a/assets/templates/main_menu_top_left.png b/assets/templates/main_menu_top_left.png new file mode 100644 index 0000000..18086c5 Binary files /dev/null and b/assets/templates/main_menu_top_left.png differ diff --git a/assets/templates_1280_720/main_menu_top_left.png b/assets/templates_1280_720/main_menu_top_left.png new file mode 100644 index 0000000..c0d207a Binary files /dev/null and b/assets/templates_1280_720/main_menu_top_left.png differ diff --git a/params.ini b/params.ini index 564e61a..c2364b9 100644 --- a/params.ini +++ b/params.ini @@ -3,8 +3,6 @@ name=Botty monitor=0 res=1280_720 -offset_top=160 -offset_left=320 max_game_length_s=320 resume_key=f11 exit_key=f12 diff --git a/src/bot.py b/src/bot.py index 73cc4d6..af8e683 100644 --- a/src/bot.py +++ b/src/bot.py @@ -26,11 +26,11 @@ import random class Bot: - def __init__(self): + def __init__(self, screen: Screen): + self._screen = screen self._config = Config() self._game_stats = GameStats() - self._game_recovery = GameRecovery() - self._screen = Screen(self._config.general["monitor"]) + self._game_recovery = GameRecovery(self._screen) self._template_finder = TemplateFinder(self._screen) self._item_finder = ItemFinder() self._ui_manager = UiManager(self._screen, self._template_finder) @@ -375,7 +375,9 @@ if __name__ == "__main__": import keyboard keyboard.add_hotkey("f12", lambda: os._exit(1)) keyboard.wait("f11") - bot = Bot() + config = Config() + screen = Screen(config.general["monitor"]) + bot = Bot(screen) bot.state = "a5_town" bot._curr_location = Location.A5_TOWN_START bot.on_maintenance() diff --git a/src/config.py b/src/config.py index 5b2c3fd..8740a69 100644 --- a/src/config.py +++ b/src/config.py @@ -28,8 +28,6 @@ class Config: "name": self._select_val("general", "name"), "monitor": int(self._select_val("general", "monitor")), "res": self._select_val("general", "res"), - "offset_top": int(self._select_val("general", "offset_top")), - "offset_left": int(self._select_val("general", "offset_left")), "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"), @@ -93,7 +91,7 @@ class Config: self.hammerdin = self._config["hammerdin"] if "hammerdin" in self._custom: self.hammerdin.update(self._custom["hammerdin"]) - + self.advanced_options = { "pathing_delay_factor": min(max(int(self._select_val("advanced_options", "pathing_delay_factor")),1),10), "template_threshold": float(self._select_val("advanced_options", "template_threshold")), @@ -150,7 +148,7 @@ if __name__ == "__main__": # cv2.imwrite(f"./assets/items/{k}.png", img) # else: # print(f"{attrib}_{base_name}=1") - + for filename in os.listdir(f'assets/items'): filename = filename.lower() if filename.endswith('.png'): diff --git a/src/game_recovery.py b/src/game_recovery.py index 8016769..eb87297 100644 --- a/src/game_recovery.py +++ b/src/game_recovery.py @@ -9,9 +9,9 @@ import time class GameRecovery: - def __init__(self): + def __init__(self, screen: Screen): self._config = Config() - self._screen = Screen(self._config.general["monitor"]) + self._screen = screen self._template_finder = TemplateFinder(self._screen) self._death_manager = DeathManager(self._screen, self._template_finder) self._ui_manager = UiManager(self._screen, self._template_finder) @@ -48,5 +48,7 @@ class GameRecovery: if __name__ == "__main__": - game_recovery = GameRecovery() + config = Config() + screen = Screen(config.general["monitor"]) + game_recovery = GameRecovery(screen) game_recovery.go_to_hero_selection() diff --git a/src/run.py b/src/run.py index ecd720a..553538f 100644 --- a/src/run.py +++ b/src/run.py @@ -1,5 +1,6 @@ from bot import Bot from game_recovery import GameRecovery +from screen import Screen from logger import Logger import keyboard import os @@ -13,11 +14,13 @@ from beautifultable import BeautifulTable import time import logging import cv2 +import traceback def run_bot(config: Config): - game_recovery = GameRecovery() - bot = Bot() + screen = Screen(config.general["monitor"]) + game_recovery = GameRecovery(screen) + bot = Bot(screen) bot_thread = threading.Thread(target=bot.start) bot_thread.start() do_restart = False @@ -42,8 +45,7 @@ def run_bot(config: Config): send_discord(f"{config.general['name']} got stuck and can not resume", config.general["custom_discord_hook"]) os._exit(1) - -if __name__ == "__main__": +def main(): config = Config(print_warnings=True) if config.general["logg_lvl"] == "info": Logger.init(logging.INFO) @@ -84,5 +86,13 @@ if __name__ == "__main__": break time.sleep(0.02) + +if __name__ == "__main__": + # To avoid cmd just closing down, except any errors and add a input() to the end + try: + main() + except: + print("RUNTIME ERROR:") + traceback.print_exc() print("Press Enter to exit ...") input() diff --git a/src/screen.py b/src/screen.py index 881b603..1fbe8b8 100644 --- a/src/screen.py +++ b/src/screen.py @@ -5,6 +5,7 @@ import time from logger import Logger from typing import Tuple from config import Config +from utils.misc import load_template import os @@ -20,15 +21,27 @@ class Screen: if monitor_idx >= len(self._sct.monitors): Logger.warning("Monitor index not available! Choose a smaller number for 'monitor' in the param.ini. Forcing value to 0 for now.") monitor_idx = 1 - config = Config() + self._config = Config() self._monitor_roi = self._sct.monitors[monitor_idx] - # For windowed screens it is expected to always have them at the top left edge and adjust offset_top then - self._monitor_roi["top"] += config.general["offset_top"] - self._monitor_roi["left"] += config.general["offset_left"] - self._monitor_roi["width"] = config.ui_pos["screen_width"] - self._monitor_roi["height"] = config.ui_pos["screen_height"] - self._monitor_x_range = (self._monitor_roi["left"] + 10, self._monitor_roi["left"] + self._monitor_roi["width"] - 10) - self._monitor_y_range = (self._monitor_roi["top"] + 10, self._monitor_roi["top"] + self._monitor_roi["height"] - 10) + # auto find offests + res_str = "" if self._config.general['res'] == "1920_1080" else "_1280_720" + template = load_template(f"assets/templates{res_str}/main_menu_top_left.png", 1.0) + img = self.grab() + self._sct = mss() + res = cv2.matchTemplate(img, template, cv2.TM_CCOEFF_NORMED) + _, max_val, _, max_pos = cv2.minMaxLoc(res) + if max_val > 0.9: + offset_left, offset_top = max_pos + Logger.debug(f"Set offsets: left {offset_left}px, top {offset_top}px") + self._monitor_roi["top"] += offset_top + self._monitor_roi["left"] += offset_left + self._monitor_x_range = (self._monitor_roi["left"] + 10, self._monitor_roi["left"] + self._monitor_roi["width"] - 10) + self._monitor_y_range = (self._monitor_roi["top"] + 10, self._monitor_roi["top"] + self._monitor_roi["height"] - 10) + self._monitor_roi["width"] = self._config.ui_pos["screen_width"] + self._monitor_roi["height"] = self._config.ui_pos["screen_height"] + else: + Logger.error("Could not find top left corner of window to set offset, shutting down") + raise RuntimeError("Could not determine window offset") def convert_monitor_to_screen(self, screen_coord: Tuple[float, float]) -> Tuple[float, float]: return (screen_coord[0] - self._monitor_roi["left"], screen_coord[1] - self._monitor_roi["top"]) @@ -62,7 +75,7 @@ if __name__ == "__main__": while 1: start = time.time() test_img = screen.grab().copy() - print(time.time() - start) + # print(time.time() - start) show_roi = True show_pt = True diff --git a/src/shop/ias_gloves.py b/src/shop/ias_gloves.py index aab1bcd..914eb36 100644 --- a/src/shop/ias_gloves.py +++ b/src/shop/ias_gloves.py @@ -12,9 +12,9 @@ import screen from config import Config from logger import Logger from npc_manager import NpcManager, Npc -from template_finder import TemplateFinder, load_template +from template_finder import TemplateFinder from utils.custom_mouse import mouse -from utils.misc import wait +from utils.misc import wait, load_template diff --git a/src/template_finder.py b/src/template_finder.py index 6404a90..cf90dbd 100644 --- a/src/template_finder.py +++ b/src/template_finder.py @@ -6,15 +6,9 @@ from logger import Logger import time import os from config import Config +from utils.misc import load_template -def load_template(path, scale_factor): - if os.path.isfile(path): - template_img = cv2.imread(path) - template_img = cv2.resize(template_img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_NEAREST) - return template_img - return None - class TemplateFinder: def __init__(self, screen: Screen, scale_factor: float = None): """ @@ -145,12 +139,12 @@ class TemplateFinder: return self._templates[key][0] def search( - self, + self, ref: Union[str, np.ndarray], inp_img: np.ndarray, - threshold: float = None, + threshold: float = None, roi: List[float] = None, - normalize_monitor: bool = False, + normalize_monitor: bool = False, ) -> Tuple[bool, Tuple[float, float]]: """ Search for a template in an image diff --git a/src/utils/auto_settings.py b/src/utils/auto_settings.py index eb7f1c3..20d99f8 100644 --- a/src/utils/auto_settings.py +++ b/src/utils/auto_settings.py @@ -1,11 +1,24 @@ import json import os +from config import Config +from mss import mss + def adjust_settings(): # You might not belive it, but there are cases where users press f9 and just dont read any console output and think everything is done # so for now, removing this warning as it seems there has never been anybody actually backing up the original settings anyway... # print("Warning: This will overwrite some of your graphics and gameplay settings. D2R must not be running during this action! Continue with Enter...") # input() + # find monitor res + config = Config() + sct = mss() + monitor_idx = config.general["monitor"] + 1 # sct saves the whole screen (including both monitors if available at index 0, then monitor 1 at 1 and 2 at 2) + if len(sct.monitors) == 1: + print("How do you not have a monitor connected?!") + os._exit(1) + if monitor_idx >= len(sct.monitors): + monitor_idx = 1 + monitor_res = f"{sct.monitors[monitor_idx]['width']}_{sct.monitors[monitor_idx]['height']}" # Get D2r folder d2_saved_games = f"C:\\Users\\{os.getlogin()}\\Saved Games\\Diablo II Resurrected" if not os.path.exists(d2_saved_games): @@ -20,6 +33,17 @@ def adjust_settings(): new_settings = json.load(f) for key in new_settings: curr_settings[key] = new_settings[key] + # catch error where user sets a higher botty res than the monitor + if monitor_res == "1280_720" and config.general["res"] == "1920_1080": + print("ERROR: You can not set 'res' to 1920_1080 while your monitor is in 1280_720") + return + # In case monitor res is at 720p, force fullscreen + if monitor_res == config.general["res"]: + print(f"Detected param res and monitor res to be the same ({monitor_res}). Forcing fullscreen mode.") + curr_settings["Window Mode"] = 1 + if monitor_res == "1920_1080": + # most template screenshots where take with this setting in 1080p: + curr_settings["Anti Aliasing"] = 1 # write back to settings.json with open(d2_saved_games + "\\Settings.json", 'w') as outfile: json.dump(curr_settings, outfile) diff --git a/src/utils/misc.py b/src/utils/misc.py index 754ca76..8d93cee 100644 --- a/src/utils/misc.py +++ b/src/utils/misc.py @@ -5,6 +5,7 @@ from logger import Logger import cv2 from typing import List, Tuple import requests +import os from version import __version__ @@ -49,3 +50,10 @@ def hms(seconds: int): m = seconds % 3600 // 60 s = seconds % 3600 % 60 return '{:02d}:{:02d}:{:02d}'.format(h, m, s) + +def load_template(path, scale_factor): + if os.path.isfile(path): + template_img = cv2.imread(path) + template_img = cv2.resize(template_img, None, fx=scale_factor, fy=scale_factor, interpolation=cv2.INTER_NEAREST) + return template_img + return None diff --git a/src/utils/node_recorder.py b/src/utils/node_recorder.py index 091ebfd..4264c67 100644 --- a/src/utils/node_recorder.py +++ b/src/utils/node_recorder.py @@ -1,7 +1,8 @@ from screen import Screen import cv2 from config import Config -from template_finder import TemplateFinder, load_template +from template_finder import TemplateFinder +from utils.misc import load_template import mouse import keyboard import os diff --git a/test/hero_select.png b/test/hero_select.png new file mode 100644 index 0000000..d79cbaf Binary files /dev/null and b/test/hero_select.png differ diff --git a/test/smoke_test.py b/test/smoke_test.py index 81548c0..bf97ed8 100644 --- a/test/smoke_test.py +++ b/test/smoke_test.py @@ -1,5 +1,14 @@ from logger import Logger from bot import Bot +from screen import Screen +from config import Config +import cv2 + + +class ScreenMock(Screen): + def grab(self): + img = cv2.imread("test/hero_select.png") + return img class TestSmoke: @@ -11,4 +20,5 @@ class TestSmoke: Logger.remove_file_logger() def test_smoke(self): - bot = Bot() + screen = ScreenMock() + bot = Bot(screen)