# Fix: On Windows, Python 3.8+ requires os.add_dll_directory for conda-forge DLLs. # Must run BEFORE any import that might trigger tesserocr loading. import os, sys if sys.platform == "win32": for _d in [ os.path.join(sys.prefix, "Library", "bin"), os.path.join(sys.prefix, "Library", "mingw-w64", "bin"), os.path.join(sys.prefix, "Library", "usr", "bin"), ]: if os.path.isdir(_d): os.add_dll_directory(_d) # Fix: OpenSSL 3.x + corrupted Windows cert store causes aiohttp to crash at import time. # Monkey-patch ssl.create_default_context to skip Windows cert store loading. if sys.platform == "win32": import ssl _orig_load_default_certs = ssl.SSLContext.load_default_certs def _safe_load_default_certs(self, purpose=ssl.Purpose.CLIENT_AUTH): try: _orig_load_default_certs(self, purpose) except ssl.SSLError: # Windows cert store is corrupted, use certifi instead try: import certifi self.load_verify_locations(certifi.where()) except Exception: pass # No certs available, continue without verification ssl.SSLContext.load_default_certs = _safe_load_default_certs from dataclasses import dataclass from input_layer import keyboard from beautifultable import BeautifulTable import logging import traceback import screen import string from version import __version__ from config import Config from logger import Logger from game_controller import GameController from utils.graphic_debugger import GraphicDebuggerController from utils.misc import restore_d2r_window_visibility from utils.auto_settings import adjust_settings, backup_settings, restore_settings_from_backup from utils.os_detect import detect_os from ui.run_selector import open_run_selector @dataclass class Controllers(): game: GameController debugger: GraphicDebuggerController def start_or_pause_bot(controllers: Controllers): if controllers.game.is_running: controllers.game.toggle_pause_bot() screen.stop_detecting_window() else: # Kill any other controllers and start botty controllers.debugger.stop() screen.start_detecting_window() controllers.game.start() def start_or_stop_graphic_debugger(controllers: Controllers): if controllers.debugger.is_running: controllers.debugger.stop() screen.stop_detecting_window() else: # Kill any other controller and start debugger screen.start_detecting_window() controllers.game.stop() controllers.debugger.start() def on_exit(controllers: Controllers): Logger.info('Force Exit') # Generate session report before exiting if controllers.game.game_stats: try: controllers.game.game_stats._save_session_report() except Exception as e: Logger.warning(f"Failed to save session report: {e}") screen.stop_detecting_window() restore_d2r_window_visibility() os._exit(1) def _log_platform_info(): """Log Windows version, mouse mode, and OCR backend at startup.""" os_info = detect_os() build = f" (build {os_info.build})" if os_info.build else "" Logger.info(f"Platform: {os_info.label}{build}") if not os_info.supported: Logger.warning(f"Unsupported OS detected: {os_info.system} {os_info.release}") Logger.info(f"Environment profile: {os_info.environment_file}") Logger.info(f"Requirements profile: {os_info.requirements_file}") from input_layer.win_input import _USE_ABSOLUTE_MOUSE mouse_mode = "absolute (Windows 10)" if _USE_ABSOLUTE_MOUSE else "relative (Windows 11)" Logger.info(f"Mouse input mode: {mouse_mode}") # OCR backend check try: from d2r_image.ocr import PyTessBaseAPI, OEM if PyTessBaseAPI is not None: Logger.info("OCR backend: tesserocr (primary)") else: try: import pytesseract Logger.warning("OCR backend: pytesseract (fallback — tesserocr missing)") except ImportError: Logger.error("OCR backend: NONE — neither tesserocr nor pytesseract is installed") except Exception as e: Logger.error(f"OCR backend check failed: {e}") def startup_checks(): _log_platform_info() # check if paths contain non-ascii characters check_for_non_ascii = { "D2R path": Config().general["d2r_path"], "Windows username": os.getlogin(), "Botty path": os.getcwd() } for key, value in check_for_non_ascii.items(): strip_punctuation = value.translate(str.maketrans('', '', string.punctuation)) if not len(strip_punctuation) == len(strip_punctuation.encode()): print(f"\n!! WARNING: {key} ({value}) contains incompatible characters. This could result in Botty encoding errors.\n") def main(): # Create folder for debug screenshots if they dont exist yet for dir_name in ["log", "log/stats", "log/runs", "log/screenshots", "log/screenshots/info", "log/screenshots/items", "log/screenshots/pickit", "log/screenshots/generated"]: os.makedirs(dir_name, exist_ok=True) controllers = Controllers( GameController(), GraphicDebuggerController() ) if Config().advanced_options["logg_lvl"] == "info": Logger.init(logging.INFO) elif Config().advanced_options["logg_lvl"] == "debug": Logger.init(logging.DEBUG) else: print(f"ERROR: Unkown logg_lvl {Config().advanced_options['logg_lvl']}. Must be one of [info, debug]") startup_checks() # Auto-launch D2R only when explicitly enabled in params.ini (auto_login=1) from utils.restart import process_exists, restart_game if not process_exists("D2R.exe"): if Config().general["auto_login"]: Logger.info("D2R is not running, launching with auto-login...") restart_game(Config().general["d2r_path"], Config().advanced_options["launch_options"]) else: Logger.info("D2R is not running and auto_login=0 — please launch D2R manually, then press the resume key to start.") else: Logger.info("D2R is already running") print(f"============ Botty {__version__} [name: {Config().general['name']}] ============") print("\nFor gettings started and documentation\nplease read https://github.com/aeon0/botty\n") table = BeautifulTable() table.set_style(BeautifulTable.STYLE_DEFAULT) table.rows.append([Config().advanced_options['select_runs_key'], "Select boss / farm runs"]) table.rows.append([Config().advanced_options['restore_settings_from_backup_key'], "Restore D2R settings from backup"]) table.rows.append([Config().advanced_options['settings_backup_key'], "Backup D2R current settings"]) table.rows.append([Config().advanced_options['auto_settings_key'], "Adjust D2R settings"]) table.rows.append([Config().advanced_options['graphic_debugger_key'], "Start / Stop Graphic debugger"]) table.rows.append([Config().advanced_options['resume_key'], "Start / Pause Botty"]) table.rows.append([Config().advanced_options['exit_key'], "Stop bot"]) table.columns.header = ["hotkey", "action"] try: print(table) except UnicodeEncodeError: # Fallback for terminals without UTF-8 support for row in table.rows: print(f" {row[0]:<10} {row[1]}") print("\n") # Set up log rotation Discord notification callback from utils import log_rotation if Config().log_rotation.get("discord_notify_rotation"): def _log_rotation_callback(deleted: int, dir_path: str, total_mb: float): from messages import Messenger m = Messenger() if m.enabled: # Normalize path for readability dname = dir_path.split(os.sep)[-1] if dir_path else "log" m.send_message(f"Log rotation: deleted {deleted} old files from {dname}/ ({total_mb:.0f}MB remaining)") log_rotation.set_rotation_callback(_log_rotation_callback) keyboard.add_hotkey(Config().advanced_options['select_runs_key'], lambda: open_run_selector(Config())) keyboard.add_hotkey(Config().advanced_options['auto_settings_key'], lambda: adjust_settings()) keyboard.add_hotkey(Config().advanced_options['graphic_debugger_key'], lambda: start_or_stop_graphic_debugger(controllers)) keyboard.add_hotkey(Config().advanced_options['restore_settings_from_backup_key'], lambda: restore_settings_from_backup()) keyboard.add_hotkey(Config().advanced_options['settings_backup_key'], lambda: backup_settings()) keyboard.add_hotkey(Config().advanced_options['resume_key'], lambda: start_or_pause_bot(controllers)) keyboard.add_hotkey(Config().advanced_options["exit_key"], lambda: on_exit(controllers)) keyboard.wait() if __name__ == "__main__": # To avoid cmd just closing down, except any errors and add a input() to the end try: game_controller = GameController() debugger_controller = GraphicDebuggerController() main() except: traceback.print_exc() # In --noconsole builds, skip input() - the hotkey-based exit_key handles shutdown try: if __import__('sys').stdin and __import__('sys').stdin.isatty(): input() except (OSError, ValueError): pass