Auto offset (#120)

* automatically calculate offsets, not-yet working

* space

* space

* float to int

* working

* clean

* clean2

* auto adjust windowed mode depending on screen res and botty res

* clean screen instance, fix tests

* remove test png

Co-authored-by: aeon0 <[email protected]>
This commit is contained in:
mgleed
2021-11-26 23:26:42 +01:00
committed by GitHub
co-authored by aeon0
parent e9c877029d
commit fdcdd90b43
16 changed files with 101 additions and 43 deletions
+1 -3
View File
@@ -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: <a href="/assets/docs/sample_graphics.png">Example 1</a>, <a href="/assets/docs/sample_graphics_2.png">Example 2</a></br>
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

-2
View File
@@ -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
+6 -4
View File
@@ -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()
+2 -4
View File
@@ -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'):
+5 -3
View File
@@ -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()
+14 -4
View File
@@ -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()
+22 -9
View File
@@ -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
+2 -2
View File
@@ -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
+4 -10
View File
@@ -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
+24
View File
@@ -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)
+8
View File
@@ -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
+2 -1
View File
@@ -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
Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

+11 -1
View File
@@ -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)