diff --git a/config/params.ini b/config/params.ini index f8e9dd5..6a38c7f 100644 --- a/config/params.ini +++ b/config/params.ini @@ -7,6 +7,15 @@ target_tz=1 saved_games_folder= level_max_steps=20 +; Battle.net credentials (for auto-login at launch) +; Leave empty to log in manually +bnet_name= +bnet_pass= + +; Character name to auto-select from the character selection screen +; If empty, bot relies on the saved character template from previous sessions +char_name= + ; messaging custom_loot_message_hook= custom_message_hook= diff --git a/src/config.py b/src/config.py index 07b4547..f788902 100644 --- a/src/config.py +++ b/src/config.py @@ -131,6 +131,9 @@ class Config: self.general = { + "bnet_name": self._select_val("general", "bnet_name"), + "bnet_pass": self._select_val("general", "bnet_pass"), + "char_name": self._select_val("general", "char_name"), "saved_games_folder": self._select_val("general", "saved_games_folder"), "name": _default_iff(self._select_val("general", "name"), "", "botty"), "max_game_length_s": float(self._select_val("general", "max_game_length_s")), @@ -361,10 +364,21 @@ class Config: "hwnd_window_process": _default_iff(Config()._select_val("advanced_options", "hwnd_window_process"), ''), "window_client_area_offset": tuple(map(int, Config()._select_val("advanced_options", "window_client_area_offset").split(","))), "ocr_during_pickit": bool(int(self._select_val("advanced_options", "ocr_during_pickit"))), - "launch_options": self._select_val("advanced_options", "launch_options").replace("", only_lowercase_letters(self.general["name"].lower())), + "launch_options": self._build_launch_options(), "override_capabilities": _default_iff(Config()._select_optional("advanced_options", "override_capabilities"), ""), } + def _build_launch_options(self) -> str: + """Build D2R launch options, appending auto-login flags when bnet credentials are set.""" + opts = self._select_val("advanced_options", "launch_options").replace( + "", only_lowercase_letters(self.general["name"].lower()) + ) + bnet_name = self.general.get("bnet_name", "") + bnet_pass = self.general.get("bnet_pass", "") + if bnet_name and bnet_pass: + opts += f" -bnetname {bnet_name} -bnetpass {bnet_pass}" + return opts + self.colors = {} for key in self.configs["game"]["parser"]["colors"]: self.colors[key] = np.split(np.array([int(x) for x in self._select_val("colors", key).split(",")]), 2) diff --git a/src/ui/character_select.py b/src/ui/character_select.py index b46df69..198604a 100644 --- a/src/ui/character_select.py +++ b/src/ui/character_select.py @@ -131,4 +131,109 @@ def select_char() -> bool: scrolls_attempts += 1 wait(0.4, 0.6) Logger.error(f"select_char: unable to find saved profile after {scrolls_attempts} scroll attempts") - return False \ No newline at end of file + + # Fallback: try OCR-based name matching + char_name = Config().general.get("char_name", "") + if char_name: + Logger.info(f"Template matching failed, trying OCR for character name: {char_name}") + return _select_char_by_ocr(char_name) + return False + + # No template saved — try OCR-based lookup if char_name is set + char_name = Config().general.get("char_name", "") + if char_name: + Logger.info(f"No template saved, selecting character '{char_name}' via OCR") + return _select_char_by_ocr(char_name) + + Logger.error("select_char: No character template saved and no char_name configured") + return False + + +def _select_char_by_ocr(char_name: str) -> bool: + """OCR-based character selection fallback. Scans visible character list, finds + the matching name, clicks it, then scrolls and retries up to 2 times.""" + char_name_lower = char_name.lower() + + # Ensure we're on the right tab (online vs offline) + online_status = None + if online_character is not None: + img = grab() + if (match := detect_screen_object(ScreenObjects.OnlineStatus, img)).valid: + online_status = online_active(match) + if online_status != online_character: + if not select_online_tab(match.region, match.center): + Logger.error("_select_char_by_ocr: could not switch online/offline tab") + return False + img = grab() + wait(1, 1.5) + else: + Logger.debug("_select_char_by_ocr: online status unknown, searching all tabs") + + if online_status is not None: + img = grab() + else: + img = grab() + + # Get the character list ROI for scrolling + char_roi = Config().ui_roi["character_select"] + + for scroll_pass in range(3): + if scroll_pass > 0: + # Scroll down and re-grab + center = roi_center(char_roi) + center = convert_screen_to_monitor(center) + mouse.move(*center) + wait(0.4, 0.6) + mouse.wheel(-14) + wait(0.4, 0.6) + img = grab() + + # OCR each visible character entry + # The character list is a vertical column — scan rows + x, y, w, h = char_roi + row_height = 30 # approximate height of each character entry row + num_rows = max(1, h // row_height) + + for row in range(num_rows): + row_y = y + row * row_height + row_h = min(row_height + 5, y + h - row_y) + if row_h < 5: + break + + roi = [x, row_y, w, row_h] + row_img = cut_roi(img, roi) + + try: + result = ocr.image_to_text( + images=row_img, + model="hover-eng_inconsolata_inv_th_fast", + psm=6, + scale=1.2, + crop_pad=False, + erode=False, + invert=False, + threshold=0, + digits_only=False, + fix_regexps=True, + check_known_errors=False, + correct_words=False, + ) + if result and result[0].text: + detected = result[0].text.strip().lower() + Logger.debug(f"Row {row}: OCR detected '{detected}'") + if char_name_lower == detected: + click_x = x + w // 2 + click_y = row_y + row_h // 2 + click_pos = convert_screen_to_monitor((click_x, click_y)) + Logger.info(f"Found character '{char_name}' at row {row}") + mouse.move(*click_pos) + wait(0.4, 0.6) + mouse.click(button="left") + wait(0.4, 0.6) + return True + except Exception as e: + Logger.debug(f"OCR row {row} failed: {e}") + continue + + Logger.error(f"_select_char_by_ocr: could not find '{char_name}' after scanning all visible entries") + return False