diff --git a/resources b/resources index c87c40af..40cc0b6f 160000 --- a/resources +++ b/resources @@ -1 +1 @@ -Subproject commit c87c40af1372d2f3a1f80f4d9687527ef28bf57a +Subproject commit 40cc0b6f0a422f6579b09e106dad56a8b932547a diff --git a/src/gui/menu/file.py b/src/gui/menu/file.py index d0bd77fa..d82db545 100644 --- a/src/gui/menu/file.py +++ b/src/gui/menu/file.py @@ -11,8 +11,16 @@ def __init__(self, parent, **kwargs): super().__init__(parent, 'File', **kwargs) # parent.add_cascade(label='File', menu=self) - self.add_command(label='New Routine', command=utils.async_callback(self, File._new_routine)) - self.add_command(label='Save Routine', command=utils.async_callback(self, File._save_routine)) + self.add_command( + label='New Routine', + command=utils.async_callback(self, File._new_routine), + state=tk.DISABLED + ) + self.add_command( + label='Save Routine', + command=utils.async_callback(self, File._save_routine), + state=tk.DISABLED + ) self.add_separator() self.add_command(label='Load Command Book', command=utils.async_callback(self, File._load_commands)) self.add_command( @@ -22,6 +30,8 @@ def __init__(self, parent, **kwargs): ) def enable_routine_state(self): + self.entryconfig('New Routine', state=tk.NORMAL) + self.entryconfig('Save Routine', state=tk.NORMAL) self.entryconfig('Load Routine', state=tk.NORMAL) @staticmethod diff --git a/src/modules/bot.py b/src/modules/bot.py index 3d8f5e20..baeccde5 100644 --- a/src/modules/bot.py +++ b/src/modules/bot.py @@ -5,6 +5,8 @@ import git import cv2 import inspect +import importlib +import traceback from os.path import splitext, basename from src.common import config, utils from src.detection import detection @@ -141,13 +143,15 @@ def _solve_rune(self, model): threshold=0.9) if rune_buff: rune_buff_pos = min(rune_buff, key=lambda p: p[0]) - target = tuple(round(rune_buff_pos[i] + config.capture.window[i]) - for i in range(2)) + target = ( + round(rune_buff_pos[0] + config.capture.window['left']), + round(rune_buff_pos[1] + config.capture.window['top']) + ) click(target, button='right') + self.rune_active = False break elif len(solution) == 4: inferences.append(solution) - self.rune_active = False def load_commands(self, file): """Prompts the user to select a command module to import. Updates config's command book.""" @@ -168,7 +172,17 @@ def load_commands(self, file): # Import the desired command book file module_name = splitext(basename(file))[0] target = '.'.join(['resources', 'command_books', module_name]) - module = __import__(target, fromlist=['']) + try: + module = importlib.import_module(target) + module = importlib.reload(module) + except ImportError: # Display errors in the target Command Book + print(' ! Errors during compilation:\n') + for line in traceback.format_exc().split('\n'): + line = line.rstrip() + if line: + print(' ' * 4 + line) + print(f"\n ! Command book '{module_name}' was not loaded") + return # Check if the 'step' function has been implemented step_found = False @@ -200,7 +214,7 @@ def load_commands(self, file): if not step_found and not movement_found: print(f" ! Error: Must either implement both 'Move' and 'Adjust' commands, " - f"or the function 'step'.") + f"or the function 'step'") if required_found and (step_found or movement_found): self.module_name = module_name self.command_book = new_cb @@ -209,11 +223,9 @@ def load_commands(self, file): config.gui.menu.file.enable_routine_state() config.gui.view.status.set_cb(basename(file)) config.routine.clear() - print(f" ~ Successfully loaded command book '{module_name}'.") - return True + print(f" ~ Successfully loaded command book '{module_name}'") else: - print(f" ! Command book '{module_name}' was not loaded.") - return False + print(f" ! Command book '{module_name}' was not loaded") def update_submodules(self, force=False): print('\n[~] Retrieving latest submodules:') diff --git a/src/modules/capture.py b/src/modules/capture.py index 21cf4016..0d8bf0cf 100644 --- a/src/modules/capture.py +++ b/src/modules/capture.py @@ -4,10 +4,11 @@ import cv2 import threading import ctypes +import mss +import mss.windows import numpy as np from src.common import config, utils from ctypes import wintypes -from PIL import ImageGrab user32 = ctypes.windll.user32 user32.SetProcessDPIAware() @@ -16,7 +17,7 @@ MINIMAP_TOP_BORDER = 5 # The thickness of the other three borders of the minimap -MINIMAP_BOTTOM_BORDER = 8 +MINIMAP_BOTTOM_BORDER = 9 # Offset in pixels to adjust for windowed mode WINDOWED_OFFSET_TOP = 36 @@ -50,8 +51,13 @@ def __init__(self): self.minimap = {} self.minimap_ratio = 1 self.minimap_sample = None - self.window = (0, 0, 1366, 768) - self.scale = 1.0 + self.sct = None + self.window = { + 'left': 0, + 'top': 0, + 'width': 1366, + 'height': 768 + } self.ready = False self.calibrated = False @@ -67,81 +73,74 @@ def start(self): def _main(self): """Constantly monitors the player's position and in-game events.""" + mss.windows.CAPTUREBLT = 0 while True: - if not self.calibrated: - handle = user32.FindWindowW(None, 'MapleStory') - rect = wintypes.RECT() - user32.GetWindowRect(handle, ctypes.pointer(rect)) - rect = (rect.left, rect.top, rect.right, rect.bottom) - rect = tuple(max(0, x) for x in rect) - - # Preliminary window to template match minimap - self.scale = ctypes.windll.shcore.GetScaleFactorForDevice(0) / 100 - self.window = ( - rect[0], - rect[1], - max(rect[2], rect[0] + MMT_WIDTH), # Make room for minimap templates - max(rect[3], rect[1] + MMT_HEIGHT) - ) - - # Calibrate by finding the bottom right corner of the minimap + # Calibrate screen capture + handle = user32.FindWindowW(None, 'MapleStory') + rect = wintypes.RECT() + user32.GetWindowRect(handle, ctypes.pointer(rect)) + rect = (rect.left, rect.top, rect.right, rect.bottom) + rect = tuple(max(0, x) for x in rect) + + self.window['left'] = rect[0] + self.window['top'] = rect[1] + self.window['width'] = max(rect[2] - rect[0], MMT_WIDTH) + self.window['height'] = max(rect[3] - rect[1], MMT_HEIGHT) + + # Calibrate by finding the bottom right corner of the minimap + with mss.mss() as self.sct: self.frame = self.screenshot() - if self.frame is None: - continue - self.frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR) - tl, _ = utils.single_match(self.frame, MM_TL_TEMPLATE) - _, br = utils.single_match(self.frame, MM_BR_TEMPLATE) - mm_tl = ( - tl[0] + MINIMAP_BOTTOM_BORDER, - tl[1] + MINIMAP_TOP_BORDER - ) - mm_br = ( - max(mm_tl[0] + PT_WIDTH, br[0] - MINIMAP_BOTTOM_BORDER), - max(mm_tl[1] + PT_HEIGHT, br[1] - MINIMAP_BOTTOM_BORDER - 1) - ) - - # Resize window to encompass minimap if needed - self.window = ( - rect[0], - rect[1], - max(rect[2], mm_br[0]), - max(rect[3], mm_br[1]) - ) - self.minimap_ratio = (mm_br[0] - mm_tl[0]) / (mm_br[1] - mm_tl[1]) - self.minimap_sample = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] - self.calibrated = True - - # Take screenshot - self.frame = self.screenshot() if self.frame is None: continue - self.frame = cv2.cvtColor(self.frame, cv2.COLOR_RGB2BGR) - - # Crop the frame to only show the minimap - minimap = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] - - # Determine the player's position - player = utils.multi_match(minimap, PLAYER_TEMPLATE, threshold=0.8) - if player: - config.player_pos = utils.convert_to_relative(player[0], minimap) - - # Package display information to be polled by GUI - self.minimap = { - 'minimap': minimap, - 'rune_active': config.bot.rune_active, - 'rune_pos': config.bot.rune_pos, - 'path': config.path, - 'player_pos': config.player_pos - } - - if not self.ready: - self.ready = True - time.sleep(0.001) + tl, _ = utils.single_match(self.frame, MM_TL_TEMPLATE) + _, br = utils.single_match(self.frame, MM_BR_TEMPLATE) + mm_tl = ( + tl[0] + MINIMAP_BOTTOM_BORDER, + tl[1] + MINIMAP_TOP_BORDER + ) + mm_br = ( + max(mm_tl[0] + PT_WIDTH, br[0] - MINIMAP_BOTTOM_BORDER), + max(mm_tl[1] + PT_HEIGHT, br[1] - MINIMAP_BOTTOM_BORDER) + ) + self.minimap_ratio = (mm_br[0] - mm_tl[0]) / (mm_br[1] - mm_tl[1]) + self.minimap_sample = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] + self.calibrated = True + + with mss.mss() as self.sct: + while True: + if not self.calibrated: + break + + # Take screenshot + self.frame = self.screenshot() + if self.frame is None: + continue + + # Crop the frame to only show the minimap + minimap = self.frame[mm_tl[1]:mm_br[1], mm_tl[0]:mm_br[0]] + + # Determine the player's position + player = utils.multi_match(minimap, PLAYER_TEMPLATE, threshold=0.8) + if player: + config.player_pos = utils.convert_to_relative(player[0], minimap) + + # Package display information to be polled by GUI + self.minimap = { + 'minimap': minimap, + 'rune_active': config.bot.rune_active, + 'rune_pos': config.bot.rune_pos, + 'path': config.path, + 'player_pos': config.player_pos + } + + if not self.ready: + self.ready = True + time.sleep(0.001) def screenshot(self, delay=1): try: - return np.array(ImageGrab.grab(self.window)) - except OSError: + return np.array(self.sct.grab(self.window)) + except mss.exception.ScreenShotError: print(f'\n[!] Error while taking screenshot, retrying in {delay} second' + ('s' if delay != 1 else '')) time.sleep(delay)