diff --git a/combat_engine.py b/combat_engine.py index bb7e942..1176c7f 100644 --- a/combat_engine.py +++ b/combat_engine.py @@ -4,7 +4,7 @@ import time import logging import pyautogui -import pydirectinput +from hardware_control import hw_ctrl as pydirectinput from config import GameConfig from text_finder import TextFinder from combat_detector import CombatDetector diff --git a/coordinate_patrol.py b/coordinate_patrol.py index 88ef947..1ff9277 100644 --- a/coordinate_patrol.py +++ b/coordinate_patrol.py @@ -5,7 +5,7 @@ import math import random import time import logging -import pydirectinput +from hardware_control import hw_ctrl as pydirectinput from stuck_handler import StuckHandler # 转向与死区常量(度) diff --git a/hardware_control.py b/hardware_control.py index db98567..2cc94af 100644 --- a/hardware_control.py +++ b/hardware_control.py @@ -1,10 +1,26 @@ +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 @@ -15,6 +31,25 @@ class HardwareController: 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) @@ -47,6 +82,37 @@ class HardwareController: 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: @@ -56,14 +122,33 @@ class HardwareController: except Exception: pass - def __init__(self, dll_path="ddl/wyhkm.dll"): - if self._wyhkm: - return + def _normalize_hardware_key(self, key_str): + return str(key_str).strip().upper() - self.dll_path = self._resolve_resource_path(dll_path) + 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: {dll_path}") - return + self._log(f">>> [hardware_control] DLL not found: {self._dll_path_setting}") + self._wyhkm = None + return False try: hkmdll = windll.LoadLibrary(self.dll_path) @@ -71,27 +156,22 @@ class HardwareController: if hkmdll.DllInstall(1, 2) < 0: self._log(">>> [hardware_control] DllInstall failed") - return - - self._log(f">>> [hardware_control] DllInstall ok: {self.dll_path}") + self._wyhkm = None + return False pythoncom.CoInitialize() - try: - self._wyhkm = win32com.client.Dispatch("wyp.hkm") - except Exception as e: - self._log(f">>> [hardware_control] COM Dispatch failed: {e}") - return + 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 + return False if not self._wyhkm.Open(dev_id, 0): self._log(">>> [hardware_control] Open device failed") self._wyhkm = None - return + return False self._wyhkm.SetMode(1, 1) self._wyhkm.SetMode(2, 1) @@ -99,104 +179,211 @@ class HardwareController: self._wyhkm.SetMouseInterval(30, 50) self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}") - except Exception as e: - self._log(f">>> [hardware_control] Init failed: {e}") + 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 not self._wyhkm: - return False - try: - return self._wyhkm.IsOpen(0) - except Exception: - return False + if self.uses_hardware_input(): + return self._ensure_hardware_ready() + return self._directinput_available() def delay_rnd(self, min_ms, max_ms): - if self.is_available(): + 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.is_available(): + 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: - self._wyhkm.KeyDown(str(key_str).upper()) - except Exception: - pass + 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.is_available(): + 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: - self._wyhkm.KeyUp(str(key_str).upper()) - except Exception: - pass + 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.is_available(): + 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: - self._wyhkm.KeyPress(str(key_str).upper()) - except Exception: - pass + 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): - self.key_down(key_str) + return self.key_down(key_str) def keyUp(self, key_str): - self.key_up(key_str) + return self.key_up(key_str) def press(self, key_str): - self.key_press(key_str) + return self.key_press(key_str) def move_to(self, x, y): - if self.is_available(): + 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: - return bool(self._wyhkm.MoveTo(int(x), int(y))) - except Exception as e: - self._log(f">>> [hardware_control] MoveTo failed ({x}, {y}): {e}") + 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.is_available(): + 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: - return bool(self._wyhkm.MoveR(int(dx), int(dy))) - except Exception as e: - self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {e}") + 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): - self.move_r(dx, dy) + return self.move_r(dx, dy) def left_click(self): - if self.is_available(): + 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: - return bool(self._wyhkm.LeftClick()) - except Exception as e: - self._log(f">>> [hardware_control] LeftClick failed: {e}") + 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.is_available(): + 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: - return bool(self._wyhkm.RightClick()) - except Exception as e: - self._log(f">>> [hardware_control] RightClick failed: {e}") + 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.is_available(): + 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: - return bool(self._wyhkm.LeftDown()) - except Exception as e: - self._log(f">>> [hardware_control] LeftDown failed: {e}") + 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.is_available(): + 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: - return bool(self._wyhkm.LeftUp()) - except Exception as e: - self._log(f">>> [hardware_control] LeftUp failed: {e}") + pydirectinput.mouseUp(button="left") + return True + except Exception as exc: + self._log(f">>> [directinput] left mouseUp failed: {exc}") return False diff --git a/requirements.txt b/requirements.txt index 5e93e3f..2489fb8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ opencv-python==4.9.0.80 numpy==1.26.4 pyautogui==0.9.54 +pydirectinput>=1.0.4 Pillow==10.2.0 pygetwindow==0.0.9 pyinstaller>=6.0.0 diff --git a/wow_multikey_gui.py b/wow_multikey_gui.py index ec41e87..bca8b0d 100644 --- a/wow_multikey_gui.py +++ b/wow_multikey_gui.py @@ -323,6 +323,7 @@ class GameLoopWorker(QThread): release_spirit_key=None, resurrect_key=None, enable_mouse_loot=True, + use_hardware_input=True, ): super().__init__() self.mode = mode # 'monitor' | 'patrol' | 'combat' | 'quest_follow' | 'flight' | 'record' @@ -362,14 +363,25 @@ class GameLoopWorker(QThread): self.release_spirit_key = release_spirit_key self.resurrect_key = resurrect_key self.enable_mouse_loot = enable_mouse_loot + self.use_hardware_input = bool(use_hardware_input) def run(self): try: from game_state import parse_game_state, format_game_state_line + from hardware_control import hw_ctrl except ImportError: self.log_signal.emit("❌ 无法导入 game_state 模块") return + hw_ctrl.configure(use_hardware_input=self.use_hardware_input) + backend_text = { + "hardware": "硬件", + "hardware_unavailable": "硬件(不可用)", + "directinput": "pydirectinput", + "directinput_unavailable": "pydirectinput(不可用)", + }.get(hw_ctrl.backend_label(), "未知") + self.log_signal.emit(f"⌨️ 控制方式: {backend_text}") + if self.mode == 'patrol': try: from auto_bot_move import AutoBotMove, load_waypoints @@ -1076,6 +1088,8 @@ class WoWMultiKeyGUI(QMainWindow): self.gs_resurrect_key.setText("0") self.gs_enable_mouse_loot = QCheckBox("启用扫雷拾取") self.gs_enable_mouse_loot.setChecked(True) + self.gs_use_hardware_input = QCheckBox("启用硬件控制(关闭则用 pydirectinput)") + self.gs_use_hardware_input.setChecked(True) # 网格填充 game_grid.addWidget(QLabel("剥皮等待时间:"), 0, 0) @@ -1103,9 +1117,10 @@ class WoWMultiKeyGUI(QMainWindow): game_grid.addWidget(QLabel("释放灵魂按键:"), 4, 2) game_grid.addWidget(self.gs_release_spirit_key, 4, 3) - game_grid.addWidget(self.gs_bag_full_hearthstone, 5, 1) + game_grid.addWidget(self.gs_use_hardware_input, 5, 0, 1, 2) game_grid.addWidget(QLabel("复活按键:"), 5, 2) game_grid.addWidget(self.gs_resurrect_key, 5, 3) + game_grid.addWidget(self.gs_bag_full_hearthstone, 6, 1) params_layout.addWidget(game_group) @@ -1158,12 +1173,14 @@ class WoWMultiKeyGUI(QMainWindow): self.eat_hp_threshold_spin.setValue(int(bot_cfg.get('eat_hp_threshold', 30))) self.eat_max_wait_spin.setValue(float(bot_cfg.get('eat_max_wait_sec', 30.0))) self.gs_enable_mouse_loot.setChecked(bool(bot_cfg.get('enable_mouse_loot', True))) + self.gs_use_hardware_input.setChecked(bool(bot_cfg.get('use_hardware_input', True))) except Exception: self.skinning_wait_spin.setValue(1.5) self.food_key_edit.setText('f1') self.eat_hp_threshold_spin.setValue(30) self.eat_max_wait_spin.setValue(30.0) self.gs_enable_mouse_loot.setChecked(True) + self.gs_use_hardware_input.setChecked(True) def _save_params_config(self): """保存「参数配置」界面到 game_state_config.json(多分辨率)并写入 wow_multikey_qt.json(bot 参数)""" @@ -1192,7 +1209,10 @@ class WoWMultiKeyGUI(QMainWindow): self.config['bot']['eat_hp_threshold'] = int(self.eat_hp_threshold_spin.value()) self.config['bot']['eat_max_wait_sec'] = float(self.eat_max_wait_spin.value()) self.config['bot']['enable_mouse_loot'] = self.gs_enable_mouse_loot.isChecked() + self.config['bot']['use_hardware_input'] = self.gs_use_hardware_input.isChecked() self._save_main_config() + from hardware_control import hw_ctrl + hw_ctrl.configure(use_hardware_input=self.gs_use_hardware_input.isChecked()) self.log(f"✅ 参数配置已保存至 {path},并更新 bot 参数") QMessageBox.information(self, "保存成功", f"参数配置已保存至:\n{path}\n\nBot 参数已写入:\n{self.config_path}") @@ -1640,6 +1660,7 @@ class WoWMultiKeyGUI(QMainWindow): resurrect_key = '0' enable_mouse_loot = self.gs_enable_mouse_loot.isChecked() + use_hardware_input = self.gs_use_hardware_input.isChecked() self.game_worker = GameLoopWorker( mode, waypoints_path=waypoints_path, vendor_path=vendor_path, @@ -1660,6 +1681,7 @@ class WoWMultiKeyGUI(QMainWindow): release_spirit_key=release_spirit_key, resurrect_key=resurrect_key, enable_mouse_loot=enable_mouse_loot, + use_hardware_input=use_hardware_input, ) self.game_worker.state_signal.connect(self.state_label.setText) self.game_worker.log_signal.connect(self.log)