import json import os import random import sys import time from ctypes import c_long, c_longlong, windll import pythoncom import win32com.client try: import pydirectinput except Exception as exc: pydirectinput = None _PYDIRECTINPUT_IMPORT_ERROR = exc else: _PYDIRECTINPUT_IMPORT_ERROR = None pydirectinput.FAILSAFE = False pydirectinput.PAUSE = 0 MAIN_CONFIG_FILE = "wow_multikey_qt.json" class HardwareController: _instance = None _wyhkm = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(HardwareController, cls).__new__(cls) return cls._instance def __init__(self, dll_path="ddl/wyhkm.dll", use_hardware_input=None): if getattr(self, "_bootstrapped", False): if dll_path: self._dll_path_setting = dll_path if use_hardware_input is not None: self.configure(use_hardware_input=use_hardware_input, dll_path=dll_path) return self._bootstrapped = True self._dll_path_setting = dll_path self._backend = "hardware" self._last_backend_log = None self._last_directinput_error = None self.dll_path = None if use_hardware_input is None: use_hardware_input = self._load_backend_preference(default=True) self.configure(use_hardware_input=use_hardware_input, dll_path=dll_path) def _runtime_base_dir(self): if getattr(sys, "frozen", False): return os.path.dirname(sys.executable) return os.path.dirname(os.path.abspath(__file__)) def _resolve_resource_path(self, relative_path): if os.path.isabs(relative_path): return relative_path if os.path.exists(relative_path) else None candidates = [] if getattr(sys, "frozen", False): exe_dir = os.path.dirname(sys.executable) if exe_dir: candidates.append(os.path.join(exe_dir, relative_path)) meipass = getattr(sys, "_MEIPASS", "") if meipass: candidates.append(os.path.join(meipass, relative_path)) module_dir = os.path.dirname(os.path.abspath(__file__)) candidates.append(os.path.join(module_dir, relative_path)) candidates.append(os.path.abspath(relative_path)) seen = set() for candidate in candidates: candidate = os.path.abspath(candidate) if candidate in seen: continue seen.add(candidate) if os.path.exists(candidate): return candidate return None def _config_path(self): candidates = [os.path.join(self._runtime_base_dir(), MAIN_CONFIG_FILE)] if getattr(sys, "frozen", False): meipass = getattr(sys, "_MEIPASS", "") if meipass: candidates.append(os.path.join(meipass, MAIN_CONFIG_FILE)) candidates.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), MAIN_CONFIG_FILE)) seen = set() for candidate in candidates: candidate = os.path.abspath(candidate) if candidate in seen: continue seen.add(candidate) if os.path.exists(candidate): return candidate return os.path.join(self._runtime_base_dir(), MAIN_CONFIG_FILE) def _load_backend_preference(self, default=True): config_path = self._config_path() if not os.path.exists(config_path): return default try: with open(config_path, "r", encoding="utf-8") as f: cfg = json.load(f) bot_cfg = (cfg or {}).get("bot") or {} return bool(bot_cfg.get("use_hardware_input", default)) except Exception as exc: self._log(f">>> [input_control] load config failed: {exc}") return default def _log(self, message): print(message) try: log_path = os.path.join(self._runtime_base_dir(), "hardware_control.log") with open(log_path, "a", encoding="utf-8") as f: f.write(message + "\n") except Exception: pass def _normalize_hardware_key(self, key_str): return str(key_str).strip().upper() def _normalize_directinput_key(self, key_str): return str(key_str).strip().lower() def _directinput_available(self): if pydirectinput is not None: return True if self._last_directinput_error != _PYDIRECTINPUT_IMPORT_ERROR: self._last_directinput_error = _PYDIRECTINPUT_IMPORT_ERROR self._log(f">>> [input_control] pydirectinput unavailable: {_PYDIRECTINPUT_IMPORT_ERROR}") return False def _ensure_hardware_ready(self): if self._wyhkm: try: if self._wyhkm.IsOpen(0): return True except Exception: self._wyhkm = None self.dll_path = self._resolve_resource_path(self._dll_path_setting) if not self.dll_path: self._log(f">>> [hardware_control] DLL not found: {self._dll_path_setting}") self._wyhkm = None return False try: hkmdll = windll.LoadLibrary(self.dll_path) hkmdll.DllInstall.argtypes = (c_long, c_longlong) if hkmdll.DllInstall(1, 2) < 0: self._log(">>> [hardware_control] DllInstall failed") self._wyhkm = None return False pythoncom.CoInitialize() self._wyhkm = win32com.client.Dispatch("wyp.hkm") dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0) if dev_id == -1: self._log(">>> [hardware_control] Device not found (Index 0)") self._wyhkm = None return False if not self._wyhkm.Open(dev_id, 0): self._log(">>> [hardware_control] Open device failed") self._wyhkm = None return False self._wyhkm.SetMode(1, 1) self._wyhkm.SetMode(2, 1) self._wyhkm.SetKeyInterval(30, 50) self._wyhkm.SetMouseInterval(30, 50) self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}") return True except Exception as exc: self._log(f">>> [hardware_control] Init failed: {exc}") self._wyhkm = None return False def _log_backend(self): label = self.backend_label() if label != self._last_backend_log: self._last_backend_log = label self._log(f">>> [input_control] backend={label}") def configure(self, use_hardware_input=True, dll_path=None): if dll_path: self._dll_path_setting = dll_path self._backend = "hardware" if bool(use_hardware_input) else "directinput" if self._backend == "hardware": self._ensure_hardware_ready() else: self._directinput_available() self._log_backend() return self._backend def uses_hardware_input(self): return self._backend == "hardware" def backend_label(self): if self.uses_hardware_input(): return "hardware" if self._ensure_hardware_ready() else "hardware_unavailable" return "directinput" if self._directinput_available() else "directinput_unavailable" def is_available(self): if self.uses_hardware_input(): return self._ensure_hardware_ready() return self._directinput_available() def delay_rnd(self, min_ms, max_ms): if self.uses_hardware_input() and self.is_available(): try: self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) except Exception: pass return if self._directinput_available(): low = min(int(min_ms), int(max_ms)) high = max(int(min_ms), int(max_ms)) time.sleep(random.uniform(low, high) / 1000.0) def key_down(self, key_str): if self.uses_hardware_input(): if self.is_available(): try: self._wyhkm.KeyDown(self._normalize_hardware_key(key_str)) return True except Exception as exc: self._log(f">>> [hardware_control] KeyDown failed ({key_str}): {exc}") return False if self._directinput_available(): try: pydirectinput.keyDown(self._normalize_directinput_key(key_str)) return True except Exception as exc: self._log(f">>> [directinput] keyDown failed ({key_str}): {exc}") return False def key_up(self, key_str): if self.uses_hardware_input(): if self.is_available(): try: self._wyhkm.KeyUp(self._normalize_hardware_key(key_str)) return True except Exception as exc: self._log(f">>> [hardware_control] KeyUp failed ({key_str}): {exc}") return False if self._directinput_available(): try: pydirectinput.keyUp(self._normalize_directinput_key(key_str)) return True except Exception as exc: self._log(f">>> [directinput] keyUp failed ({key_str}): {exc}") return False def key_press(self, key_str): if self.uses_hardware_input(): if self.is_available(): try: self._wyhkm.KeyPress(self._normalize_hardware_key(key_str)) return True except Exception as exc: self._log(f">>> [hardware_control] KeyPress failed ({key_str}): {exc}") return False if self._directinput_available(): try: pydirectinput.press(self._normalize_directinput_key(key_str)) return True except Exception as exc: self._log(f">>> [directinput] press failed ({key_str}): {exc}") return False def keyDown(self, key_str): return self.key_down(key_str) def keyUp(self, key_str): return self.key_up(key_str) def press(self, key_str): return self.key_press(key_str) def move_to(self, x, y): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.MoveTo(int(x), int(y))) except Exception as exc: self._log(f">>> [hardware_control] MoveTo failed ({x}, {y}): {exc}") return False if self._directinput_available(): try: pydirectinput.moveTo(int(x), int(y)) return True except Exception as exc: self._log(f">>> [directinput] moveTo failed ({x}, {y}): {exc}") return False def move_r(self, dx, dy): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.MoveR(int(dx), int(dy))) except Exception as exc: self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {exc}") return False if self._directinput_available(): try: pydirectinput.moveRel(int(dx), int(dy)) return True except Exception as exc: self._log(f">>> [directinput] moveRel failed ({dx}, {dy}): {exc}") return False def MoveR(self, dx, dy): return self.move_r(dx, dy) def left_click(self): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.LeftClick()) except Exception as exc: self._log(f">>> [hardware_control] LeftClick failed: {exc}") return False if self._directinput_available(): try: pydirectinput.click(button="left") return True except Exception as exc: self._log(f">>> [directinput] left click failed: {exc}") return False def right_click(self): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.RightClick()) except Exception as exc: self._log(f">>> [hardware_control] RightClick failed: {exc}") return False if self._directinput_available(): try: pydirectinput.click(button="right") return True except Exception as exc: self._log(f">>> [directinput] right click failed: {exc}") return False def left_down(self): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.LeftDown()) except Exception as exc: self._log(f">>> [hardware_control] LeftDown failed: {exc}") return False if self._directinput_available(): try: pydirectinput.mouseDown(button="left") return True except Exception as exc: self._log(f">>> [directinput] left mouseDown failed: {exc}") return False def left_up(self): if self.uses_hardware_input(): if self.is_available(): try: return bool(self._wyhkm.LeftUp()) except Exception as exc: self._log(f">>> [hardware_control] LeftUp failed: {exc}") return False if self._directinput_available(): try: pydirectinput.mouseUp(button="left") return True except Exception as exc: self._log(f">>> [directinput] left mouseUp failed: {exc}") return False hw_ctrl = HardwareController()