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
[](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)