Add switchable hardware input backend
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
# 转向与死区常量(度)
|
||||
|
||||
@@ -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
|
||||
|
||||
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.uses_hardware_input():
|
||||
if self.is_available():
|
||||
try:
|
||||
self._wyhkm.KeyDown(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
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(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
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(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
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):
|
||||
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.uses_hardware_input():
|
||||
if self.is_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}")
|
||||
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 e:
|
||||
self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {e}")
|
||||
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):
|
||||
self.move_r(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 e:
|
||||
self._log(f">>> [hardware_control] LeftClick failed: {e}")
|
||||
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 e:
|
||||
self._log(f">>> [hardware_control] RightClick failed: {e}")
|
||||
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 e:
|
||||
self._log(f">>> [hardware_control] LeftDown failed: {e}")
|
||||
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 e:
|
||||
self._log(f">>> [hardware_control] LeftUp failed: {e}")
|
||||
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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user