def test_search(self): """Check that different :py:class:`FileResolver` instances contain the same paths.""" self.resolver.add_path("images") self.assertEqual("images/shape_black_box.png", self.resolver.search("shape_black_box.png")) new_finder = FileResolver() self.assertEqual("images/shape_black_box.png", new_finder.search("shape_black_box"))
def setUp(self): """Create mocks and enable patches.""" # start with a clean environment self._old_paths = list(FileResolver._target_paths) FileResolver().clear() self._tmpfiles = [] self.stepsfile_content = "" self._patches = { # Chain class and it's parent checks the existence of some files, so let's # report that they are all there -- except for one that we need to be missing "path_exists": patch("os.path.exists", lambda f: f not in self.non_existing_files), # The Target class will build a match file for each item in the stepsfile "Finder_from_match_file": patch("guibot.finder.Finder.from_match_file", wraps=self._get_match_file), "Finder_to_match_file": patch("guibot.finder.Finder.to_match_file"), "PIL_Image_open": patch("PIL.Image.open") } self.mock_exists = self._patches["path_exists"].start() self.mock_match_read = self._patches["Finder_from_match_file"].start() self.mock_match_write = self._patches["Finder_to_match_file"].start() # this one is not that important -- no need to store self._patches["PIL_Image_open"].start() return super().setUp()
def test_custom_paths(self): """Test if custom paths work correctly.""" # temporary directory 1 tmp_dir1 = mkdtemp() fd_tmp_file1, tmp_file1 = mkstemp(prefix=tmp_dir1 + "/", suffix=".txt") os.close(fd_tmp_file1) # temporary directory 2 tmp_dir2 = mkdtemp() fd_tmp_file2, tmp_file2 = mkstemp(prefix=tmp_dir2 + "/", suffix=".txt") os.close(fd_tmp_file2) filename1 = os.path.basename(tmp_file1) filename2 = os.path.basename(tmp_file2) try: file_resolver = FileResolver() file_resolver.add_path(tmp_dir1) file_resolver.add_path(tmp_dir2) # sanity check - assert that normal path resolution works self.assertEqual(file_resolver.search(filename1), tmp_file1) self.assertEqual(file_resolver.search(filename2), tmp_file2) # now check that only one of these are found in our scope with CustomFileResolver(tmp_dir2) as p: self.assertEqual(p.search(filename2), tmp_file2) self.assertRaises(FileNotFoundError, p.search, filename1) # finally check that we've correctly restored everything on exit self.assertEqual(file_resolver.search(filename1), tmp_file1) self.assertEqual(file_resolver.search(filename2), tmp_file2) finally: # clean up FileResolver().remove_path(os.path.dirname(tmp_dir1)) shutil.rmtree(tmp_dir1) shutil.rmtree(tmp_dir2)
def setUpClass(cls): cls.file_resolver = FileResolver() cls.file_resolver.add_path(os.path.join(common_test.unittest_dir, 'images')) # preserve values of static attributes cls.prev_loglevel = GlobalConfig.image_logging_level cls.prev_logpath = GlobalConfig.image_logging_destination GlobalConfig.image_logging_level = 0 GlobalConfig.image_logging_destination = os.path.join(common_test.unittest_dir, 'tmp')
def tearDown(self): """Cleanup removing any patches and files created.""" # start with a clean environment for p in self._old_paths: FileResolver().add_path(p) # stop patches for p in self._patches.values(): p.stop() for fn in self._tmpfiles: os.unlink(fn) return super().tearDown()
def __init__(self, game, debug_mode: bool = False): super().__init__() self._game = game self._debug_mode = debug_mode # The dimensions are set in calibrate_game_window() in Game class. self._window_left = None self._window_top = None self._window_width = None self._window_height = None # Initialize the following for saving screenshots. self._image_number = 0 self._new_folder_name = None # Initialize GuiBot object for image matching. self._guibot = GuiBot() self._file_resolver = FileResolver() # Initialize EasyOCR for text detection. self._game.print_and_save(f"\n{self._game.printtime()} [INFO] Initializing EasyOCR reader...") self._reader = easyocr.Reader(["en"], gpu=True) self._game.print_and_save(f"{self._game.printtime()} [INFO] EasyOCR reader initialized.")
# to be reused as a tool for matching fixed needle/haystack # pairs in order to figure out the best parameter configuration # for successful matching. import logging import shutil from guibot.config import GlobalConfig from guibot.imagelogger import ImageLogger from guibot.fileresolver import FileResolver from guibot.errors import * from guibot.target import * from guibot.finder import * # Parameters to toy with file_resolver = FileResolver() file_resolver.add_path('images/') BACKEND = "template" # could be Text('Text') or any other target type NEEDLE = Image('shape_blue_circle') HAYSTACK = Image('all_shapes') # image logging variables LOGPATH = './tmp/' REMOVE_LOGPATH = False # Overall logging setup handler = logging.StreamHandler() logging.getLogger('').addHandler(handler) logging.getLogger('').setLevel(logging.DEBUG) formatter = logging.Formatter( '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
def setUp(self): self.resolver = FileResolver() # Clear paths from any previous unit test since # the paths are shared between all FileResolver instances self.resolver.clear()
class FileResolverTest(unittest.TestCase): """Tests for the FileResolverTest class.""" @classmethod def setUpClass(cls): # Change to 'tests' directory cls.saved_working_dir = os.getcwd() os.chdir(common_test.unittest_dir) @classmethod def tearDownClass(cls): os.chdir(cls.saved_working_dir) def setUp(self): self.resolver = FileResolver() # Clear paths from any previous unit test since # the paths are shared between all FileResolver instances self.resolver.clear() def test_deprecated_class(self): """Check that the deprecated :py:class:`Path` class still works.""" logger = logging.getLogger("guibot.path") # import the legacy path module should log a warning with mock.patch.object(logger, "warn") as mock_warn: mock_warn.assert_not_called() from guibot.path import Path # TODO replace by assert_called_once when support for Python 3.5 is dropped self.assertEqual(len(mock_warn.mock_calls), 1) self.assertEqual(Path, FileResolver) def test_add_path(self): """Test that adding a path works.""" self.resolver.add_path("paths") def test_remove_path(self): """Test that removing a path works.""" self.resolver.add_path("images") self.assertEqual(True, self.resolver.remove_path("images")) self.assertEqual(False, self.resolver.remove_path("images")) def test_remove_unknown_path(self): """Check that removing unknown paths doesn't break anything.""" self.resolver.remove_path("foobar_does_not_exist") def test_search(self): """Check that different :py:class:`FileResolver` instances contain the same paths.""" self.resolver.add_path("images") self.assertEqual("images/shape_black_box.png", self.resolver.search("shape_black_box.png")) new_finder = FileResolver() self.assertEqual("images/shape_black_box.png", new_finder.search("shape_black_box")) def test_search_fail(self): """Test failed search.""" self.resolver.add_path("images") self.assertRaises(FileNotFoundError, self.resolver.search, "foobar_does_not_exist") def test_search_type(self): """Test that searching file names without extension works.""" self.resolver.add_path("images") # Test without extension self.assertEqual("images/shape_black_box.png", self.resolver.search("shape_black_box")) self.assertEqual("images/mouse down.txt", self.resolver.search("mouse down")) self.assertEqual("images/circle.steps", self.resolver.search("circle")) def test_search_precedence(self): """Check the precedence of extensions when searching.""" self.resolver.add_path("images") # Test correct precedence of the checks self.assertEqual("images/shape_blue_circle.xml", self.resolver.search("shape_blue_circle.xml")) self.assertEqual("images/shape_blue_circle.png", self.resolver.search("shape_blue_circle")) def test_search_keyword(self): """Check if the path restriction results in an empty set.""" self.resolver.add_path("images") self.assertEqual("images/shape_black_box.png", self.resolver.search("shape_black_box.png", "images")) self.assertRaises(FileNotFoundError, self.resolver.search, "shape_black_box.png", "other-images") def test_search_silent(self): """Check that we can disable exceptions from being raised when searching.""" self.resolver.add_path("images") self.assertEqual("images/shape_black_box.png", self.resolver.search("shape_black_box.png", silent=True)) # Fail if the path restriction results in an empty set target = self.resolver.search("shape_missing_box.png", silent=True) self.assertIsNone(target) def test_paths_iterator(self): """Test that the FileResolver iterator yields the correct list.""" self.assertListEqual(self.resolver._target_paths, [x for x in self.resolver])
def setUpClass(cls): cls.patfile_resolver = FileResolver() cls.patfile_resolver.add_path(os.path.join(common_test.unittest_dir, 'images')) random.seed(42)
def test_step_save(self): """Test that dumping a chain to a file works and that the content is preserved.""" # The Text target accepts either a file or a text string and we test # with both modes. For the first mode we need a real file. text_file = self._create_temp_file(prefix="some_text_file", extension=".txt") with open(text_file, "w") as fp: fp.write("ocr_string") # create real temp files for these -- they are saved using open() and we are not # mocking those calls. Also, the temp files will automatically be removed on tear down deep_csv = self._create_temp_file(prefix="item_for_deep", extension=".csv") cascade_xml = self._create_temp_file(prefix="item_for_cascade", extension=".xml") # no need to mock png files -- the Image target uses PIL.Image.save(), which we mocked # destination stepfile target_filename = self._create_temp_file(extension=".steps") stepsfile_contents = [ "item_for_contour.png some_contour_matchfile.match", "item_for_tempfeat.png some_tempfeat_matchfile.match", "item_for_feature.png some_feature_matchfile.match", "{} some_deep_matchfile.match".format(deep_csv), "17 some_deep_matchfile.match", "{} some_cascade_matchfile.match".format(cascade_xml), "item_for_template.png some_template_matchfile.match", "item_for_autopy.png some_autopy_matchfile.match", "{} some_text_matchfile.match".format( os.path.splitext(text_file)[0]), "some_text_content some_text_matchfile.match" ] expected_content = [ "item_for_contour.png item_for_contour.match", "item_for_tempfeat.png item_for_tempfeat.match", "item_for_feature.png item_for_feature.match", "{0}.csv {0}.match".format(os.path.splitext(deep_csv)[0]), "17 17.match", "{0}.xml {0}.match".format(os.path.splitext(cascade_xml)[0]), "item_for_template.png item_for_template.match", "item_for_autopy.png item_for_autopy.match", "{0}.txt {0}.match".format(os.path.splitext(text_file)[0]), "some_text_content some_text_content.match" ] source_stepsfile = self._create_temp_file( prefix=self.stepsfile_name, extension=".steps", contents=os.linesep.join(stepsfile_contents)) FileResolver().add_path(os.path.dirname(text_file)) try: chain = Chain(os.path.splitext(source_stepsfile)[0]) chain.save(target_filename) with open(target_filename, "r") as f: generated_content = f.read().splitlines() # assert that the generated steps file has the expected content self.assertEqual(generated_content, expected_content) # build a list of the match filenames generated from # the calls to `Finder.to_match_file()` generated_match_names = [] for c in self.mock_match_write.call_args_list: generated_match_names.append(c[0][1]) # get a list expected_match_names = [x.split("\t")[1] for x in expected_content] expected_match_names.insert( 0, os.path.splitext(source_stepsfile)[0] + ".match") # and assert that a match file was generated for each line # and for the steps file itself self.assertEqual(generated_match_names, expected_match_names) finally: FileResolver().remove_path(os.path.dirname(text_file))
class ImageUtils: """ Provides the utility functions needed to perform image-related actions. This utility will alternate between PyAutoGUI and GuiBot to find the template image. Attributes ---------- game (game.Game): The Game object. debug_mode (bool, optional): Optional flag to print debug messages related to this class. Defaults to False. """ def __init__(self, game, debug_mode: bool = False): super().__init__() self._game = game self._debug_mode = debug_mode # The dimensions are set in calibrate_game_window() in Game class. self._window_left = None self._window_top = None self._window_width = None self._window_height = None # Initialize the following for saving screenshots. self._image_number = 0 self._new_folder_name = None # Initialize GuiBot object for image matching. self._guibot = GuiBot() self._file_resolver = FileResolver() # Initialize EasyOCR for text detection. self._game.print_and_save(f"\n{self._game.printtime()} [INFO] Initializing EasyOCR reader...") self._reader = easyocr.Reader(["en"], gpu=True) self._game.print_and_save(f"{self._game.printtime()} [INFO] EasyOCR reader initialized.") def update_window_dimensions(self, window_left: int, window_top: int, window_width: int, window_height: int): """Updates the window dimensions for PyAutoGUI to perform faster operations in. Args: window_left (int, optional): The x-coordinate of the left edge of the region for image matching. window_top (int, optional): The y-coordinate of the top edge of the region for image matching. window_width (int, optional): The width of the region for image matching. window_height (int, optional): The height of the region for image matching. Returns: None """ self._window_left = window_left self._window_top = window_top self._window_width = window_width self._window_height = window_height return None def get_window_dimensions(self): """Get the window dimensions as a Tuple of 4 integers. Returns: (int, int, int, int): A Tuple of 4 integers consisting of (window_left, window_top, window_width, window_height). """ return (self._window_left, self._window_top, self._window_width, self._window_height) def _clear_memory_guibot(self): """Eliminates the memory leak caused by GuiBot by deleting the GuiBot object and reinitializing it. This is required before or after every single GuiBot operation, else you will run into cv::OutOfMemoryError. Returns: None """ del self._guibot self._guibot = GuiBot() return None def find_button(self, button_name: str, custom_confidence: float = 0.9, grayscale_check: bool = False, confirm_location_check: bool = False, tries: int = 3, sleep_time: int = 1, suppress_error: bool = False): """Find the location of the specified button. Args: button_name (str): Name of the button image file in the /images/buttons/ folder. custom_confidence (float, optional): Accuracy threshold for matching. Defaults to 0.9. grayscale_check (bool, optional): Match by converting screenshots to grayscale. This may lead to inaccuracies however. Defaults to False. confirm_location_check (bool, optional): Check to see if the location is correct. Defaults to False. tries (int, optional): Number of tries before failing. Defaults to 3. sleep_time (int, optional): Number of seconds for execution to pause for in cases of image match fail. Defaults to 1. suppress_error (bool, optional): Suppresses template matching error if True. Defaults to False. Returns: location (int, int): Tuple of coordinates of where the center of the button is located if image matching was successful. Otherwise, return None. """ button_location = None guibot_check = False while (button_location == None): if(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None): button_location = pyautogui.locateCenterOnScreen(f"images/buttons/{button_name.lower()}.png", confidence=custom_confidence, grayscale=grayscale_check, region=(self._window_left, self._window_top, self._window_width, self._window_height)) else: button_location = pyautogui.locateCenterOnScreen(f"images/buttons/{button_name.lower()}.png", confidence=custom_confidence, grayscale=grayscale_check) if (button_location == None): # Use GuiBot to template match if PyAutoGUI failed. self._file_resolver.add_path("images/buttons/") self._clear_memory_guibot() button_location = self._guibot.exists(f"{button_name.lower()}") if(button_location == None): tries -= 1 if (tries <= 0): if(not suppress_error): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Failed to find the {button_name.upper()} button.") return None if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Could not locate the {button_name.upper()} button. Trying again in {sleep_time} seconds...") time.sleep(sleep_time) else: guibot_check = True # If the location was successfully found using GuiBot, convert the Match object to a Location object. if(guibot_check): button_location = (button_location.target.x, button_location.target.y) if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Found the {button_name.upper()} button at {button_location}.") if (confirm_location_check): self.confirm_location(button_name) return button_location def confirm_location(self, location_name: str, custom_confidence: float = 0.9, grayscale_check: bool = False, tries: int = 3, sleep_time: int = 1): """Confirm the bot's position by searching for the header image. Args: location_name (str): Name of the header image file in the /images/headers/ folder. custom_confidence (float, optional): Accuracy threshold for matching. Defaults to 0.9. grayscale_check (bool, optional): Match by converting screenshots to grayscale. This may lead to inaccuracies however. Defaults to False. tries (int, optional): Number of tries before failing. Defaults to 3. sleep_time (int, optional): Number of seconds for execution to pause for in cases of image match fail. Defaults to 1. Returns: (bool): True if current location is confirmed. Otherwise, False. """ header_location = None while (header_location == None): if(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None): header_location = pyautogui.locateCenterOnScreen(f"images/headers/{location_name.lower()}_header.png", confidence=custom_confidence, grayscale=grayscale_check, region=(self._window_left, self._window_top, self._window_width, self._window_height)) else: header_location = pyautogui.locateCenterOnScreen(f"images/headers/{location_name.lower()}_header.png", confidence=custom_confidence, grayscale=grayscale_check) if (header_location == None): # Use GuiBot to template match if PyAutoGUI failed. self._file_resolver.add_path("images/headers/") self._clear_memory_guibot() header_location = self._guibot.exists(f"{location_name.lower()}_header") if(header_location == None): tries -= 1 if (tries <= 0): # If tries ran out, return False. if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Failed to confirm the bot's location at {location_name.upper()}.") return False if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Could not confirm the bot's location at {location_name.upper()}. Trying again in {sleep_time} seconds...") time.sleep(sleep_time) if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Bot's current location is at {location_name.upper()}.") return True def find_summon(self, summon_list: Iterable[str], summon_element_list: Iterable[str], home_button_x: int, home_button_y: int, custom_confidence: float = 0.9, grayscale_check: bool = False, suppress_error: bool = False): """Find the location of the specified Summon. Will attempt to scroll the screen down to see more Summons if the initial screen position yielded no matches. Args: summon_list (Iterable[str]): List of names of the Summon image's file name in /images/summons/ folder. summon_element_name (Iterable[str]): List of names of the Summon element image file in the /images/buttons/ folder. home_button_x (int): X coordinate of where the center of the Home Button is. home_button_y (int): Y coordinate of where the center of the Home Button is. custom_confidence (float, optional): Accuracy threshold for matching. Defaults to 0.9. grayscale_check (bool, optional): Match by converting screenshots to grayscale. This may lead to inaccuracies however. Defaults to False. suppress_error (bool, optional): Suppresses template matching error if True. Defaults to False. Returns: summon_location (int, int): Tuple of coordinates of where the center of the Summon is located if image matching was successful. Otherwise, return None. """ if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [DEBUG] Received the following list of Summons to search for: {str(summon_list)}") self._game.print_and_save(f"{self._game.printtime()} [DEBUG] Received the following list of Elements: {str(summon_element_list)}") summon_location = None guibot_check = False summon_index = 0 while(summon_location == None and summon_index <= len(summon_list)): # First select the Summon Element tab at the current index. self._game.print_and_save(f"{self._game.printtime()} [INFO] Now attempting to find: {summon_list[summon_index].upper()}") current_summon_element = summon_element_list[summon_index] self._game.find_and_click_button(f"summon_{current_summon_element}") while (summon_location == None and summon_index <= len(summon_list)): # Now try and find the Summon at the current index. if(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None): summon_location = pyautogui.locateCenterOnScreen(f"images/summons/{summon_list[summon_index]}.png", confidence=custom_confidence, grayscale=grayscale_check, region=(self._window_left, self._window_top, self._window_width, self._window_height)) else: summon_location = pyautogui.locateCenterOnScreen(f"images/summons/{summon_list[summon_index]}.png", confidence=custom_confidence, grayscale=grayscale_check) if (summon_location == None): # Use GuiBot to template match if PyAutoGUI failed. self._file_resolver.add_path("images/summons/") self._clear_memory_guibot() summon_location = self._guibot.exists(f"{summon_list[summon_index]}") if(summon_location == None): if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Could not locate {summon_list[summon_index].upper()} Summon. Trying again...") # If the bot reached the bottom of the page, scroll back up to the top and start searching for the next Summon. if(((self._game.farming_mode.lower() != "event" and self._game.farming_mode.lower() != "event (token drawboxes)" and self._game.farming_mode.lower() != "guild wars") and self.find_button("bottom_of_summon_selection", tries=1) != None) or ((self._game.farming_mode.lower() == "event" or self._game.farming_mode.lower() == "event (token drawboxes)" or self._game.farming_mode.lower() == "guild wars") and self.find_button("bottom_of_event_summon_selection", tries=1) != None)): self._game.mouse_tools.scroll_screen(home_button_x, home_button_y - 50, 10000) summon_index += 1 break # If matching failed, scroll the screen down to see more Summons. self._game.mouse_tools.scroll_screen(home_button_x, home_button_y - 50, -700) else: guibot_check = True if(summon_location == None and (summon_index + 1) > len(summon_list)): if(not suppress_error): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Could not find any of the specified Summons.") return None # If the location was successfully found using GuiBot, convert the Match object to a Location object. if(guibot_check): summon_location = (summon_location.target.x, summon_location.target.y) self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Found {summon_list[summon_index].upper()} Summon at {summon_location}.") return summon_location def find_dialog(self, attack_button_x: int, attack_button_y: int, custom_confidence: float = 0.9, grayscale_check: bool = False, tries: int = 3, sleep_time: int = 1): """Attempt to find any Lyria/Vyrn dialog popups. Used during Combat Mode. Args: attack_button_x (int): X coordinate of where the center of the Attack Button is. attack_button_y (int): Y coordinate of where the center of the Attack Button is. custom_confidence (float, optional): Accuracy threshold for matching. Defaults to 0.9. grayscale_check (bool, optional): Match by converting screenshots to grayscale. This may lead to inaccuracies however. Defaults to False. tries (int, optional): Number of tries before failing. Defaults to 3. sleep_time (int, optional): Number of seconds for execution to pause for in cases of image match fail. Defaults to 1. Returns: (int, int): Tuple of coordinates on the screen for where the dialog popup's center was found. Otherwise, return None. """ lyria_dialog_location = None vyrn_dialog_location = None guibot_check = False while (lyria_dialog_location == None and vyrn_dialog_location == None): if(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None): lyria_dialog_location = pyautogui.locateCenterOnScreen(f"images/dialogs/dialog_lyria.png", confidence=custom_confidence, grayscale=grayscale_check, region=(attack_button_x - 350, attack_button_y + 28, attack_button_x - 264, attack_button_y + 50)) vyrn_dialog_location = pyautogui.locateCenterOnScreen(f"images/dialogs/dialog_vyrn.png", confidence=custom_confidence, grayscale=grayscale_check, region=(attack_button_x - 350, attack_button_y + 28, attack_button_x - 264, attack_button_y + 50)) else: lyria_dialog_location = pyautogui.locateCenterOnScreen(f"images/dialogs/dialog_lyria.png", confidence=custom_confidence, grayscale=grayscale_check) vyrn_dialog_location = pyautogui.locateCenterOnScreen(f"images/dialogs/dialog_vyrn.png", confidence=custom_confidence, grayscale=grayscale_check) if (lyria_dialog_location == None and vyrn_dialog_location == None): # Use GuiBot to template match if PyAutoGUI failed. self._file_resolver.add_path("images/dialogs/") self._clear_memory_guibot() lyria_dialog_location = self._guibot.exists(f"dialog_lyria") self._clear_memory_guibot() vyrn_dialog_location = self._guibot.exists(f"dialog_vyrn") if (lyria_dialog_location == None and vyrn_dialog_location == None): tries -= 1 if (tries <= 0): if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] There are no Lyria/Vyrn dialog popups detected.") return None if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [WARNING] Could not locate any Lyria/Vyrn dialog popups failed. Trying again in {sleep_time} seconds...") time.sleep(sleep_time) else: guibot_check = True # If the location was successfully found using GuiBot, convert the Match object to a Location object. if(guibot_check): if(lyria_dialog_location != None): lyria_dialog_location = (lyria_dialog_location.target.x, lyria_dialog_location.target.y) else: vyrn_dialog_location = (vyrn_dialog_location.target.x, vyrn_dialog_location.target.y) if(self._debug_mode): if(lyria_dialog_location != None): self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Found a Lyria dialog popup at {lyria_dialog_location}.") else: self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Found a Vyrn dialog popup at {vyrn_dialog_location}.") if(lyria_dialog_location != None): return lyria_dialog_location else: return vyrn_dialog_location def find_all(self, image_name: str, custom_region: Iterable[Tuple[int, int, int, int]] = None, custom_confidence: float = 0.9, grayscale_check: bool = False, hide_info: bool = False): """Find the specified image file by searching through all subfolders and locating all occurrences on the screen. Args: image_name (str): Name of the image file in the /images/ folder. custom_region (tuple[int, int, int, int]): Region tuple of integers to look for a occurrence in. Defaults to None. custom_confidence (float, optional): Accuracy threshold for matching. Defaults to 0.9. grayscale_check (bool, optional): Match by converting screenshots to grayscale. This may lead to inaccuracies however. Defaults to False. hide_info (bool, optional): Whether or not to print the matches' locations. Defaults to False. Returns: locations (list[Box]): List of Boxes where each occurrence is found on the screen. If no occurrence was found, return a empty list. Or if the file does not exist, return None. """ dir_path = os.path.dirname(os.path.realpath(__file__)) # Find the specified image file by searching through the subfolders in the /images/ folder. for root, dirs, files in os.walk(f"{dir_path}/images/"): for file in files: file_name = os.path.splitext(str(file))[0] if (file_name.lower() == image_name.lower()): if(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None and custom_region == None): locations = list(pyautogui.locateAllOnScreen(f"{root}/{image_name}.png", confidence=custom_confidence, grayscale=grayscale_check, region=(self._window_left, self._window_top, self._window_width, self._window_height))) elif(self._window_left != None or self._window_top != None or self._window_width != None or self._window_height != None and custom_region != None): locations = list(pyautogui.locateAllOnScreen(f"{root}/{image_name}.png", confidence=custom_confidence, grayscale=grayscale_check, region=custom_region)) else: locations = list(pyautogui.locateAllOnScreen(f"{root}/{image_name}.png", confidence=custom_confidence, grayscale=grayscale_check)) centered_locations = [] if(len(locations) != 0): for (index, location) in enumerate(locations): if(index > 0): # Filter out duplicate locations where they are 1 pixel away from each other. if(location[0] != (locations[index - 1][0] + 1) and location[1] != (locations[index - 1][1] + 1)): centered_locations.append(pyautogui.center(location)) else: centered_locations.append(pyautogui.center(location)) if(not hide_info): for location in centered_locations: self._game.print_and_save(f"{self._game.printtime()} [INFO] Occurrence for {image_name.upper()} found at: " + str(location)) else: if(self._debug_mode): self._game.print_and_save(f"{self._game.printtime()} [DEBUG] Failed to detect any occurrences of {image_name.upper()} images.") return centered_locations self._game.print_and_save(f"{self._game.printtime()} [ERROR] Specified file does not exist inside the /images/ folder or its subfolders.") return None def find_farmed_items(self, item_list: Iterable[str]): """Detect amounts of items gained according to the desired items specified. Args: item_list (Iterable[str]): List of items desired to be farmed. Returns: amounts_farmed (Iterable[int]): List of amounts gained for items in order according to the given item_list. """ self._file_resolver.add_path("images/items/") # List of items blacklisted from using GuiBot's built-in CV finder due to how similar looking they are. These items have to use my method using # PyAutoGUI instead for the confidence argument from OpenCV as GuiBot does not have a confidence argument. blacklisted_items = ["Fire Orb", "Water Orb", "Earth Orb", "Wind Orb", "Light Orb", "Dark Orb", "Red Tome", "Blue Tome", "Brown Tome", "Green Tome", "White Tome", "Black Tome", "Hellfire Scroll", "Flood Scroll", "Thunder Scroll", "Gale Scroll", "Skylight Scroll", "Chasm Scroll", "Jasper Scale", "Crystal Spirit", "Luminous Judgment", "Sagittarius Rune", "Sunlight Quartz", "Shadow Silver", "Ifrit Anima", "Ifrit Omega Anima", "Cocytus Anima", "Cocytus Omega Anima", "Vohu Manah Anima", "Vohu Manah Omega Anima", "Sagittarius Anima", "Sagittarius Omega Anima", "Corow Anima", "Corow Omega Anima", "Diablo Anima", "Diablo Omega Anima", "Ancient Ecke Sachs", "Ecke Sachs", "Ancient Auberon", "Auberon", "Ancient Perseus", "Perseus", "Ancient Nalakuvara", "Nalakuvara", "Ancient Bow of Artemis", "Bow of Artemis", "Ancient Cortana", "Cortana"] lite_blacklisted_items = ["Infernal Garnet", "Frozen Hell Prism", "Evil Judge Crystal", "Horseman's Plate", "Halo Light Quartz", "Phantom Demon Jewel", "Tiamat Anima", "Tiamat Omega Anima", "Colossus Anima", "Colossus Omega Anima", "Leviathan Anima", "Leviathan Omega Anima", "Yggdrasil Anima", "Yggdrasil Omega Anima", "Luminiera Anima", "Luminiera Omega Anima", "Celeste Anima", "Celeste Omega Anima", "Shiva Anima", "Shiva Omega Anima", "Europa Anima", "Europa Omega Anima", "Alexiel Anima", "Alexiel Omega Anima", "Grimnir Anima", "Grimnir Omega Anima", "Metatron Anima", "Metatron Omega Anima", "Avatar Anima", "Avatar Omega Anima", "Nezha Anima", "Nezha Omega Anima", "Twin Elements Anima", "Twin Elements Omega Anima", "Macula Marius Anima", "Macula Marius Omega Anima", "Medusa Anima", "Medusa Omega Anima", "Apollo Anima", "Apollo Omega Anima", "Dark Angel Olivia Anima", "Dark Angel Olivia Omega Anima", "Garuda Anima", "Garuda Omega Anima", "Athena Anima", "Athena Omega Anima", "Grani Anima", "Grani Omega Anima", "Baal Anima", "Baal Omega Anima", "Odin Anima", "Odin Omega Anima", "Lich Anima", "Lich Omega Anima", "Morrigna Anima", "Morrigna Omega Anima", "Prometheus Anima", "Prometheus Omega Anima", "Ca Ong Anima", "Ca Ong Omega Anima", "Gilgamesh Anima", "Gilgamesh Omega Anima", "Hector Anima", "Hector Omega Anima", "Anubis Anima", "Anubis Omega Anima", "Huanglong Anima", "Huanglong Omega Anima", "Qilin Anima", "Qilin Omega Anima", "Tiamat Malice Anima", "Leviathan Malice Anima", "Phronesis Anima"] self._game.print_and_save(f"{self._game.printtime()} [INFO] Now detecting item rewards...") amounts_farmed = [] guibot_check = False for item in item_list: total_amount_farmed = 0 # Detect amounts gained from each item on the Loot Collected screen. If the item is on the blacklist, use my method instead. if(item in blacklisted_items): locations = self.find_all(item, custom_confidence=0.99) elif(item in lite_blacklisted_items): locations = self.find_all(item, custom_confidence=0.85) else: self._clear_memory_guibot() locations = self._guibot.find_all(item, timeout=1, allow_zero=True) guibot_check = True for index, location in enumerate(locations): check = False # Filter out any duplicate locations that are 1 pixels from each other when the item is in either of the blacklists. if(item in blacklisted_items or item in lite_blacklisted_items): for x in range(index): if((abs(location[0] - locations[x][0]) <= 1 and location[1] == locations[x][1]) or (abs(location[1] - locations[x][1]) and location[0] == locations[x][0]) or (abs(location[0] - locations[x][0]) and abs(location[1] - locations[x][1]))): check = True if(not check): # Deconstruct the location object into coordinates if found using GuiBot. if(item not in blacklisted_items and item not in lite_blacklisted_items): location = (location.target.x, location.target.y) if(guibot_check): self._game.print_and_save(f"{self._game.printtime()} [INFO] Occurrence for {item.upper()} found at: {location} using GuiBot.") # Adjust the width and height variables if EasyOCR cannot detect the numbers correctly. left = location[0] + 10 top = location[1] - 5 width = 30 height = 25 # Create the /temp/ folder in the /images/ folder to house the taken screenshots. current_dir = os.getcwd() temp_dir = os.path.join(current_dir, r"images/temp") if not os.path.exists(temp_dir): os.makedirs(temp_dir) # Create a screenshot in the specified region named "test" and save it in the /temp/ folder. Then use EasyOCR to extract text from it into a list. test_image = pyautogui.screenshot("images/temp/test.png", region=(left, top, width, height)) # test_image.show() # Uncomment this line of code to see what the bot captured for the region of the detected text. result = self._reader.readtext("images/temp/test.png", detail=0) # Split any unnecessary characters in the extracted text until only the number remains. result_cleaned = 0 if(len(result) != 0): result_split = [char for char in result[0]] for char in result_split: try: if(int(char)): result_cleaned = int(char) except ValueError: continue else: result_cleaned = 1 total_amount_farmed += result_cleaned else: self._game.print_and_save(f"{self._game.printtime()} [INFO] Duplicate location detected. Removing it...") amounts_farmed.append(total_amount_farmed) # If items were detected on the Quest Results screen, take a screenshot and save in the /results/ folder. if(len(amounts_farmed) > 0 and amounts_farmed[0] != 0): self._take_screenshot() self._game.print_and_save(f"{self._game.printtime()} [INFO] Detection of item rewards finished.") return amounts_farmed def wait_vanish(self, image_name: str, timeout: int = 30): """Use GuiBot to check if the provided image vanishes from the screen after a certain amount of time. Args: image_name (str): Name of the image file in the /images/buttons/ folder. timeout (int, optional): Timeout in seconds. Defaults to 30. Returns: (bool): True if the image vanishes from the screen within the allotted time or False if timeout was reached. """ self._game.print_and_save(f"\n{self._game.printtime()} [INFO] Now waiting for {image_name} to vanish from screen...") self._file_resolver.add_path("images/buttons/") try: self._clear_memory_guibot() if(self._guibot.wait_vanish(image_name, timeout=timeout)): self._game.print_and_save(f"{self._game.printtime()} [SUCCESS] Image successfully vanished from screen...") return True else: self._game.print_and_save(f"{self._game.printtime()} [WARNING] Image did not vanish from screen...") return False except Exception as e: self._game.print_and_save(f"{self._game.printtime()} [ERROR] {image_name} should have vanished from the screen after {timeout} seconds but did not. Exact error is: \n{e}") def get_button_dimensions(self, image_name: str): """Get the dimensions of a image in images/buttons/ folder. Args: image_name (str): File name of the image in images/buttons/ folder. Returns: (int, int): Tuple of the width and the height. """ image = Image.open(f"images/buttons/{image_name}.png") width, height = image.size return (width, height) def _take_screenshot(self): """Takes a screenshot of the Quest Results screen when called in find_farmed_items(). Returns: None """ self._game.print_and_save(f"{self._game.printtime()} [INFO] Taking a screenshot of the Quest Results screen...") # Construct the image file and folder name from the current date, time, and image number. current_time = datetime.datetime.now().strftime("%H-%M-%S") current_date = date.today() new_file_name = f"{current_date} {current_time} #{self._image_number}" self._image_number += 1 if(self._new_folder_name == None): self._new_folder_name = f"{current_date} {current_time}" # Take a screenshot using the calibrated window dimensions. new_image = pyautogui.screenshot(region=(self._window_left, self._window_top, self._window_width, self._window_height)) # Create the /results/ directory if it does not already exist. current_dir = os.getcwd() results_dir = os.path.join(current_dir, r"results") if not os.path.exists(results_dir): os.makedirs(results_dir) # Then create a new folder to hold this session's screenshots in. new_dir = os.path.join(current_dir, r"results", self._new_folder_name) if not os.path.exists(new_dir): os.makedirs(new_dir) # Finally, save the new image into the results directory with its new file name. new_image.save(f"./results/{self._new_folder_name}/{new_file_name}.jpg") self._game.print_and_save(f"{self._game.printtime()} [INFO] Results image saved as \"{new_file_name}.jpg\" in \"{self._new_folder_name}\" folder...") return None def generate_alert_for_captcha(self): """Displays a alert that will inform users that a CAPTCHA was detected. Returns: None """ pyautogui.alert(text="Stopping bot. Please enter the CAPTCHA yourself and play this mission manually to its completion. \n\nIt is now highly recommended that you take a break of several hours and in the future, please reduce the amount of hours that you use this program consecutively without breaks in between.", title="CAPTCHA Detected!", button="OK") return None def generate_alert(self, message: str): """Displays a alert that will inform users about various user errors that may occur. Args: message (str): The message to be displayed. Returns: None """ pyautogui.alert(text=message, button="OK") return None