Add switchable hardware input backend

This commit is contained in:
王鹏
2026-04-20 15:53:32 +08:00
parent 66b8edf8e4
commit beb1375d71
5 changed files with 278 additions and 68 deletions

View File

@@ -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