Files
my-botty/src/main.py
T
alexpolo1 2bfcf0ff02 fix: patch ssl.load_default_certs to handle corrupted Windows cert store
OpenSSL 3.x + corrupted Windows cert store causes aiohttp to crash
at import time. Monkey-patch ssl.SSLContext.load_default_certs to
fall back to certifi when Windows store fails.
2026-06-11 13:53:54 +02:00

212 lines
9.2 KiB
Python

# 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