diff --git a/README.md b/README.md index eedea60..507fc10 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ And please. I urge you to actually read that README! It will make your life a lo 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. ### 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 "Color Test Mode" to verify your 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
**Note**: There have been issues reported with image sharpening being truned on via the graphic card settings itself outside of D2R. Try turning it off when running botty. ### 2) Supported builds @@ -30,7 +30,7 @@ Open up D2R and wait till you are at the hero selection screen. Make sure the ch Download the a prebuilt release [here](https://github.com/aeon0/botty/releases). Start `run.exe` in the botty folder. Switch (ALT+TAB) to your D2R window and press the start key (default f11). You can always force stop botty with f12. -## Color Test Mode +## Graphic Debugger To check if you graphic settings are good and if the bot would pick up items there is a **Graphic Debugger Mode** built in. Start botty and press F10 (Default key). This will open up a (mostly black) window. Start a game in D2R and go to A5. You should see some templates with blue circles detected and scores printed out to the console. E.g. for 720p you should see scores higher 0.8 for the templates. To check item finding, throw some items of different types on the ground. The debug window should show the item names with black background. If you throw an item on the ground that should be picked up, it will have a red circle. The console will print out the scores for each item that would be picked up. Scores should be well above 0.9 for these items.
@@ -70,6 +70,7 @@ loot_screenshots | If 1, the bot takes a screenshot with timestamp everytime he [routes] | Descriptions --------------------------------|--------------------------------------------- run_pindle | Run Pindle in each new game. Select "1" to run it "0" to leave it out. +run_eldritch | Run Eldritch in each new game. Select "1" to run it "0" to leave it out. run_shenk | Run shenk in each new game. Select "1" to run it "0" to leave it out. [char] | Descriptions @@ -125,10 +126,11 @@ blessed_hammer | Hotkey for Blessed Hammer [advanced_options] | Descriptions --------------------------------|--------------------------------------------- pathing_delay_factor | A linear scaling factor, between 1 and 10, applied to pathing delays. +template_threshold | Threshold to find templates used during pathing. [items] | Descriptions --------------------------------|--------------------------------------------- item_type | 0: Item will not be picked up. 1: Item will be picked up. 2: Item will be picked up and a discord message will be sent. ## Support this project -This project is free. Support it by contributing in any technical way, giving feedback, bug reports or submitting PRs. +Support it by contributing in any technical way, giving feedback, bug reports or submitting PRs. diff --git a/assets/d2r_settings.json b/assets/d2r_settings.json index 406fb75..d5202a4 100644 --- a/assets/d2r_settings.json +++ b/assets/d2r_settings.json @@ -5,7 +5,7 @@ "MaxLuminance": 600, "HDRContrast": 200, "Contrast": 100, - "Screen Resolution (Windowed)": "1920x1051", + "Screen Resolution (Windowed)": "1280x720", "Resolution Scale": 100, "Sharpening": 6, "Game Resolution": 1, @@ -15,7 +15,7 @@ "VSync": 1, "Framerate Cap": 60, "Framerate Target": 0, - "Window Mode": 1, + "Window Mode": 0, "Graphic Presets": 4, "Texture Quality": 4, "Texture Anisotropy": 0, @@ -40,7 +40,7 @@ "Show Clock": 1, "Auto Gold Enabled": 0, "Chat Gem Mode": 1, - "Item Name Display": 0, + "Item Name Display": 1, "Chat Background": 0, "Always Run": 1 } diff --git a/assets/docs/sample_graphics.png b/assets/docs/sample_graphics.png new file mode 100644 index 0000000..184dffc Binary files /dev/null and b/assets/docs/sample_graphics.png differ diff --git a/assets/docs/sample_graphics_2.png b/assets/docs/sample_graphics_2.png new file mode 100644 index 0000000..fac4601 Binary files /dev/null and b/assets/docs/sample_graphics_2.png differ diff --git a/assets/docs/video_thumbnail.png b/assets/docs/video_thumbnail.png index 0ad1e17..35a37ea 100644 Binary files a/assets/docs/video_thumbnail.png and b/assets/docs/video_thumbnail.png differ diff --git a/assets/templates/shenk/shenk_17.png b/assets/templates/shenk/shenk_17.png new file mode 100644 index 0000000..da766a0 Binary files /dev/null and b/assets/templates/shenk/shenk_17.png differ diff --git a/assets/templates_1280_720/shenk/shenk_17.png b/assets/templates_1280_720/shenk/shenk_17.png new file mode 100644 index 0000000..fb01bc1 Binary files /dev/null and b/assets/templates_1280_720/shenk/shenk_17.png differ diff --git a/params.ini b/params.ini index f8c63a2..d2e83d6 100644 --- a/params.ini +++ b/params.ini @@ -1,15 +1,15 @@ +; There is detailed documentation for each parameter in the README.md [general] name=Botty monitor=0 -res=1920_1080 -offset_top=0 -offset_left=0 +res=1280_1080 +offset_top=160 +offset_left=320 max_game_length_s=320 resume_key=f11 exit_key=f12 auto_settings_key=f9 graphic_debugger_key=f10 -template_threshold=0.68 logg_lvl=info randomize_runs=0 difficulty=hell @@ -45,7 +45,8 @@ inventory_screen=i stand_still=capslock tp=f9 belt_rows=4 -show_belt=~ +;note: this is different from the default hotkey as "~" is for many keyboards not reachable without also pressing altgr +show_belt=k potion1=1 potion2=2 potion3=3 @@ -76,6 +77,7 @@ blessed_hammer=f5 [advanced_options] ;===== don't touch unless you know what you're doing ===== pathing_delay_factor=5 +template_threshold=0.68 [items] ;=====MISC===== diff --git a/src/belt_manager.py b/src/belt_manager.py index f91decb..0a98034 100644 --- a/src/belt_manager.py +++ b/src/belt_manager.py @@ -105,7 +105,7 @@ class BeltManager: "health": self._config.char["belt_hp_columns"], "mana": self._config.char["belt_mp_columns"], } - center_m = self._screen.convert_abs_to_monitor((0, 0)) + center_m = self._screen.convert_abs_to_monitor((-200, -120)) mouse.move(*center_m, randomize=100) keyboard.send(self._config.char["show_belt"]) wait(0.5) diff --git a/src/config.py b/src/config.py index e0140e5..5b2c3fd 100644 --- a/src/config.py +++ b/src/config.py @@ -35,7 +35,6 @@ class Config: "resume_key": self._select_val("general", "resume_key"), "auto_settings_key": self._select_val("general", "auto_settings_key"), "graphic_debugger_key": self._select_val("general", "graphic_debugger_key"), - "template_threshold": float(self._select_val("general", "template_threshold")), "logg_lvl": self._select_val("general", "logg_lvl"), "randomize_runs": bool(int(self._select_val("general", "randomize_runs"))), "difficulty": self._select_val("general", "difficulty"), @@ -96,7 +95,8 @@ class Config: 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) + "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")), } self.items = {} diff --git a/src/death_manager.py b/src/death_manager.py index 4732547..ac024ae 100644 --- a/src/death_manager.py +++ b/src/death_manager.py @@ -48,10 +48,10 @@ class DeathManager: # first wait a bit to make sure health manager is done with its chicken stuff which obviously failed if run_thread is not None: kill_thread(run_thread) - # clean up key presses that might be pressed in the run_thread - keyboard.release(self._config.char["stand_still"]) - wait(0.1, 0.2) - keyboard.release(self._config.char["show_items"]) + # clean up key presses that might be pressed in the run_thread + keyboard.release(self._config.char["stand_still"]) + wait(0.1, 0.2) + keyboard.release(self._config.char["show_items"]) time.sleep(6) if self._template_finder.search("D2_LOGO_HS", self._screen.grab(), roi=self._config.ui_roi["hero_selection_logo"])[0]: # in this case chicken executed and left the game, but we were still dead. diff --git a/src/game_stats.py b/src/game_stats.py index 2427b40..045ee0e 100644 --- a/src/game_stats.py +++ b/src/game_stats.py @@ -19,6 +19,7 @@ class GameStats: def _send_discord_thread(self, msg: str): if self._config.general["custom_discord_hook"]: + msg = f"{self._config.general['name']}: {msg}" send_discord_thread = threading.Thread( target=send_discord, args=(msg, self._config.general["custom_discord_hook"]) @@ -29,7 +30,7 @@ class GameStats: def log_item_pickup(self, item_name: str, send_discord: bool): self._picked_up_items.append(item_name) if send_discord: - self._send_discord_thread(f"{self._config.general['name']} just found: {item_name}") + self._send_discord_thread(f"Found {item_name}") def log_death(self): self._death_counter += 1 @@ -37,13 +38,13 @@ class GameStats: def log_chicken(self): self._chicken_counter += 1 - self._send_discord_thread(f"Winner winner chicken dinner") + self._send_discord_thread(f"You have chickened") def log_start_game(self): if self._game_counter > 0: self._save_stats_to_file() - if self._game_counter % 15 == 0: - # every 10th game send a discord update about current status + if self._game_counter % 20 == 0: + # every 20th game send a discord update about current status self._send_discord_status_update() self._game_counter += 1 self._timer = time.time() @@ -79,7 +80,7 @@ class GameStats: return msg def _send_discord_status_update(self): - msg = f"Status Report from {self._config.general['name']}:\n{self._create_msg()}\nVersion:" + msg = f"Status Report\n{self._create_msg()}\nVersion:" self._send_discord_thread(msg) def _save_stats_to_file(self): diff --git a/src/pather.py b/src/pather.py index afd8cb4..1f121da 100644 --- a/src/pather.py +++ b/src/pather.py @@ -79,8 +79,8 @@ class Pather: 122: {"ELDRITCH_2": (530, -218), "ELDRITCH_3": (-223, -178)}, 123: {"ELDRITCH_3": (-148, -498), "ELDRITCH_2": (604, -538), "ELDRITCH_4": (-163, -283)}, # Shenk - 140: {"SHENK_0": (-224, -340), "SHENK_15": (120, 20), "SHENK_1": (667, -242)}, - 141: {"SHENK_0": (-194, 66), "SHENK_15": (116, 440), "SHENK_1": (696, 161), "SHENK_2": (-251, -51)}, + 140: {"SHENK_0": (-224, -340), "SHENK_17": (-750, 353), "SHENK_15": (120, 20), "SHENK_1": (667, -242)}, + 141: {"SHENK_0": (-194, 66), "SHENK_17": (-780, 792), "SHENK_15": (116, 440), "SHENK_1": (696, 161), "SHENK_2": (-251, -51)}, 142: {"SHENK_1": (876, 564), "SHENK_2": (-78, 352), "SHENK_3": (535, -194), "SHENK_4": (-665, -155)}, 143: {"SHENK_2": (212, 758),"SHENK_3": (823, 209), "SHENK_4": (-377, 248), "SHENK_6": (-508, -103)}, 144: {"SHENK_6": (-162, 185), "SHENK_7": (721, 226)}, @@ -130,21 +130,27 @@ class Pather: while 1: img = self._screen.grab() display_img = img.copy() + template_map = {} for node_idx in self._nodes: for template_type in self._nodes[node_idx]: if filter is None or filter in template_type: - success, ref_pos_screen = self._template_finder.search(template_type, img) + if template_type in template_map: + success = True + ref_pos_screen = template_map[template_type] + else: + success, ref_pos_screen = self._template_finder.search(template_type, img) if success: + template_map[template_type] = ref_pos_screen # Get reference position of template in abs coordinates ref_pos_abs = self._screen.convert_screen_to_abs(ref_pos_screen) # Calc the abs node position with the relative coordinates (relative to ref) - # node_pos_rel = self._get_scaled_node(node_idx, template_type) - # node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs) - # node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs) - # x, y = self._screen.convert_abs_to_screen(node_pos_abs) - # cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3) - # cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA) - x, y = self._screen.convert_abs_to_monitor(ref_pos_abs) + node_pos_rel = self._get_scaled_node(node_idx, template_type) + node_pos_abs = self._convert_rel_to_abs(node_pos_rel, ref_pos_abs) + node_pos_abs = self._adjust_abs_range_to_screen(node_pos_abs) + x, y = self._screen.convert_abs_to_screen(node_pos_abs) + cv2.circle(display_img, (x, y), 5, (255, 0, 0), 3) + cv2.putText(display_img, str(node_idx), (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA) + x, y = self._screen.convert_abs_to_screen(ref_pos_abs) cv2.circle(display_img, (x, y), 5, (0, 255, 0), 3) cv2.putText(display_img, template_type, (x, y), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 0, 0), 2, cv2.LINE_AA) display_img = cv2.resize(display_img, None, fx=0.5, fy=0.5) @@ -266,5 +272,5 @@ if __name__ == "__main__": ui_manager = UiManager(screen, t_finder) char = Sorceress(config.sorceress, config.char, screen, t_finder, ui_manager, pather) # pather.traverse_nodes_fixed("pindle_save_dist", char) - pather.traverse_nodes(Location.SHENK_START, Location.SHENK_SAVE_DIST, char) - # pather._display_all_nodes_debug(filter="SHENK") + # pather.traverse_nodes(Location.SHENK_START, Location.SHENK_SAVE_DIST, char) + pather._display_all_nodes_debug(filter="SHENK_17") diff --git a/src/pickit.py b/src/pickit.py index 53b8551..3ef14de 100644 --- a/src/pickit.py +++ b/src/pickit.py @@ -29,7 +29,7 @@ class PickIt: :return: Bool if any items were picked up or not. (Does not account for picking up scrolls and pots) """ found_items = False - keyboard.send(self._config.char["show_items"], do_press=True, do_release=False) + keyboard.send(self._config.char["show_items"]) time.sleep(1.0) # sleep needed here to give d2r time to display items on screen on keypress #Creating a screenshot of the current loot if self._config.general["loot_screenshots"]: @@ -90,7 +90,7 @@ class PickIt: else: char.move((x_m, y_m)) time.sleep(0.1) - keyboard.send(self._config.char["show_items"], do_press=False, do_release=True) + keyboard.send(self._config.char["show_items"]) return found_items diff --git a/src/run.py b/src/run.py index 832a659..31846a1 100644 --- a/src/run.py +++ b/src/run.py @@ -78,6 +78,7 @@ if __name__ == "__main__": break if keyboard.is_pressed(config.general['auto_settings_key']): adjust_settings() + break elif keyboard.is_pressed(config.general['graphic_debugger_key']): run_graphic_debugger() break diff --git a/src/template_finder.py b/src/template_finder.py index 849533e..6404a90 100644 --- a/src/template_finder.py +++ b/src/template_finder.py @@ -1,5 +1,4 @@ import cv2 -from enum import Enum from screen import Screen from typing import Tuple, Union, List import numpy as np @@ -75,6 +74,7 @@ class TemplateFinder: "SHENK_13": [load_template(f"assets/templates{res_str}/shenk/shenk_13.png", self._scale_factor), self._scale_factor], "SHENK_15": [load_template(f"assets/templates{res_str}/shenk/shenk_15.png", self._scale_factor), self._scale_factor], "SHENK_16": [load_template(f"assets/templates{res_str}/shenk/shenk_16.png", self._scale_factor), self._scale_factor], + "SHENK_17": [load_template(f"assets/templates{res_str}/shenk/shenk_17.png", self._scale_factor), self._scale_factor], # Template Selectables "A5_STASH": [load_template(f"assets/templates{res_str}/a5_stash.png", self._scale_factor), self._scale_factor], "A5_WP": [load_template(f"assets/templates{res_str}/a5_wp.png", self._scale_factor), self._scale_factor], @@ -160,7 +160,7 @@ class TemplateFinder: :param roi: Region of Interest of the inp_img to restrict search area. Format [left, top, width, height] :return: Returns found flag and the position as [bool, [x, y]]. If not found, position will be None. Position in image space. """ - threshold = self._config.general["template_threshold"] if threshold is None else threshold + threshold = self._config.advanced_options["template_threshold"] if threshold is None else threshold if roi is None: # if no roi is provided roi = full inp_img roi = [0, 0, inp_img.shape[1], inp_img.shape[0]] @@ -210,7 +210,7 @@ class TemplateFinder: :param take_ss: Bool value to take screenshot on timeout or not (flag must still be set in params!) Rest of params same as TemplateFinder.search() """ - threshold = self._config.general["template_threshold"] if threshold is None else threshold + threshold = self._config.advanced_options["template_threshold"] if threshold is None else threshold Logger.debug(f"Waiting for Template {ref}") start = time.time() while 1: diff --git a/src/ui_manager.py b/src/ui_manager.py index d1acde6..819d088 100644 --- a/src/ui_manager.py +++ b/src/ui_manager.py @@ -309,8 +309,11 @@ class UiManager(): else: # make sure there is actually an item time.sleep(0.3) - slot_pos, slot_img = self.get_slot_pos_and_img(self._config, self._screen.grab(), column, row) + hovered_item = self._screen.grab() + slot_pos, slot_img = self.get_slot_pos_and_img(self._config, hovered_item, column, row) if self._slot_has_item(slot_img): + if self._config.general["info_screenshots"]: + cv2.imwrite("./info_screenshots/info_discard_item_" + time.strftime("%Y%m%d_%H%M%S") + ".png", hovered_item) mouse.press(button="left") wait(0.2, 0.4) mouse.release(button="left") diff --git a/src/utils/auto_settings.py b/src/utils/auto_settings.py index 3b73c6f..eb7f1c3 100644 --- a/src/utils/auto_settings.py +++ b/src/utils/auto_settings.py @@ -2,8 +2,10 @@ import json import os def adjust_settings(): - print("Warning: This will overwrite some of your graphics and gameplay settings. D2R must not be running during this action! Continue with Enter...") - input() + # 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() # Get D2r folder d2_saved_games = f"C:\\Users\\{os.getlogin()}\\Saved Games\\Diablo II Resurrected" if not os.path.exists(d2_saved_games): @@ -21,7 +23,7 @@ def adjust_settings(): # write back to settings.json with open(d2_saved_games + "\\Settings.json", 'w') as outfile: json.dump(curr_settings, outfile) - print("Changed settings succesfully. You can now start D2R to start the bot.") + print("Changed settings succesfully. You can now start D2R and botty.") if __name__ == "__main__":