add 硬件控制模块 (hardware_control.py) 并修复游戏状态扫描区域宽度
- 新增 wyhkm.dll 硬件盒子 COM 接口封装,支持键盘鼠标控制 - 修复 game_state_config.json 中 scan_region_width 过小导致截图越界的问题 - 添加鼠标路径录制器、硬件测试脚本等工具 - 更新多项配置默认值 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,9 +9,10 @@ import os
|
||||
import random
|
||||
import sys
|
||||
import time
|
||||
import ctypes
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
||||
QLabel, QPushButton, QCheckBox, QSpinBox, QDoubleSpinBox, QScrollArea, QTextEdit,
|
||||
QMessageBox, QTabWidget, QGroupBox, QFormLayout, QLineEdit,
|
||||
QComboBox, QTableWidget, QTableWidgetItem, QHeaderView, QAbstractItemView,
|
||||
@@ -21,6 +22,7 @@ from PyQt6.QtGui import QPainter, QPen, QColor
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32con
|
||||
from pynput import keyboard
|
||||
|
||||
|
||||
def _config_base():
|
||||
@@ -320,6 +322,7 @@ class GameLoopWorker(QThread):
|
||||
resurrection_waypoints_path=None,
|
||||
release_spirit_key=None,
|
||||
resurrect_key=None,
|
||||
enable_mouse_loot=True,
|
||||
):
|
||||
super().__init__()
|
||||
self.mode = mode # 'monitor' | 'patrol' | 'combat' | 'quest_follow' | 'flight' | 'record'
|
||||
@@ -358,6 +361,7 @@ class GameLoopWorker(QThread):
|
||||
self.resurrection_waypoints_path = resurrection_waypoints_path
|
||||
self.release_spirit_key = release_spirit_key
|
||||
self.resurrect_key = resurrect_key
|
||||
self.enable_mouse_loot = enable_mouse_loot
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -381,6 +385,7 @@ class GameLoopWorker(QThread):
|
||||
resurrection_waypoints_path=self.resurrection_waypoints_path,
|
||||
release_spirit_key=self.release_spirit_key,
|
||||
resurrect_key=self.resurrect_key,
|
||||
enable_mouse_loot=self.enable_mouse_loot,
|
||||
)
|
||||
self.bot_move._on_hearthstone_stop = self.stop_signal.emit
|
||||
except ImportError as e:
|
||||
@@ -393,6 +398,7 @@ class GameLoopWorker(QThread):
|
||||
self.bot_combat = AutoBot(
|
||||
attack_loop_path=self.attack_loop_path,
|
||||
skinning_wait_sec=self.skinning_wait_sec,
|
||||
enable_mouse_loot=self.enable_mouse_loot,
|
||||
)
|
||||
except ImportError as e:
|
||||
self.log_signal.emit(f"❌ 自动打怪依赖加载失败: {e}")
|
||||
@@ -513,6 +519,54 @@ class GameLoopWorker(QThread):
|
||||
return None
|
||||
|
||||
|
||||
class LootPathRecorderWorker(QThread):
|
||||
"""拾取轨迹录制工作线程"""
|
||||
finished_signal = pyqtSignal(int)
|
||||
log_signal = pyqtSignal(str)
|
||||
|
||||
def __init__(self, hwnd):
|
||||
super().__init__()
|
||||
self.hwnd = hwnd
|
||||
self.running = False
|
||||
self.points = []
|
||||
|
||||
def run(self):
|
||||
if not self.hwnd:
|
||||
self.log_signal.emit("❌ 未找到游戏窗口,无法录制轨迹")
|
||||
return
|
||||
|
||||
try:
|
||||
rect = win32gui.GetWindowRect(self.hwnd)
|
||||
cx = rect[0] + (rect[2] - rect[0]) // 2
|
||||
cy = rect[1] + (rect[3] - rect[1]) // 2
|
||||
|
||||
self.points = []
|
||||
self.running = True
|
||||
self.log_signal.emit("🔴 拾取轨迹录制中... 请在角色前方平滑移动鼠标")
|
||||
|
||||
while self.running:
|
||||
x, y = win32api.GetCursorPos()
|
||||
self.points.append((x - cx, y - cy))
|
||||
time.sleep(0.02)
|
||||
|
||||
# 稀疏处理并保存
|
||||
processed = self.points[::3]
|
||||
if not processed:
|
||||
self.log_signal.emit("⚠️ 未记录到有效轨迹点")
|
||||
return
|
||||
|
||||
path = get_config_path("loot_path.json")
|
||||
with open(path, 'w') as f:
|
||||
json.dump(processed, f)
|
||||
self.finished_signal.emit(len(processed))
|
||||
self.log_signal.emit(f"✅ 拾取轨迹已保存: {len(processed)} 点")
|
||||
except Exception as e:
|
||||
self.log_signal.emit(f"❌ 轨迹录制失败: {e}")
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
|
||||
|
||||
# ============ 主窗口 ============
|
||||
|
||||
class WoWMultiKeyGUI(QMainWindow):
|
||||
@@ -522,12 +576,31 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.config = self.load_config()
|
||||
self.key_workers = {}
|
||||
self.game_worker = None
|
||||
self.loot_recorder_worker = None
|
||||
self.pending_recorder = None # 停止录制后保留,便于保存
|
||||
self.hwnd = None
|
||||
|
||||
self.init_ui()
|
||||
self.find_wow_window()
|
||||
|
||||
# 初始化全局热键监听 (F8 用于拾取录制)
|
||||
self.kb_listener = keyboard.Listener(on_press=self._on_hotkey_press)
|
||||
self.kb_listener.start()
|
||||
|
||||
def _on_hotkey_press(self, key):
|
||||
"""全局热键回调"""
|
||||
try:
|
||||
if key == keyboard.Key.f8:
|
||||
# 必须在主线程 UI 操作,但 pynput 在独立线程,这里简单判断状态
|
||||
if self.loot_recorder_worker and self.loot_recorder_worker.isRunning():
|
||||
# 停止录制
|
||||
self.stop_loot_record()
|
||||
else:
|
||||
# 开始录制
|
||||
self.start_loot_record()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def init_ui(self):
|
||||
self.setWindowTitle("WoW 多功能控制器")
|
||||
self.setGeometry(100, 100, 780, 620)
|
||||
@@ -870,15 +943,41 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
loops_layout.addWidget(loop_editor)
|
||||
tabs.addTab(tab_combat_loops, "攻击循环")
|
||||
|
||||
# Tab: 拾取录制 (新增加)
|
||||
tab_loot_record = QWidget()
|
||||
loot_record_layout = QVBoxLayout(tab_loot_record)
|
||||
loot_record_group = QGroupBox("自定义拾取轨迹")
|
||||
loot_record_inner = QVBoxLayout(loot_record_group)
|
||||
loot_record_inner.addWidget(QLabel("说明:\n1. 点击下方按钮开始录制。\n2. 在游戏角色前方划出你想要的扫瞄路径。\n3. 再次点击停止录制,轨迹将自动保存。\n4. 挂机脚本将优先使用录制的路径进行拾取。"))
|
||||
|
||||
loot_record_btn_layout = QHBoxLayout()
|
||||
self.loot_record_start_btn = QPushButton("🔴 开始录制拾取轨迹")
|
||||
self.loot_record_start_btn.clicked.connect(self.start_loot_record)
|
||||
self.loot_record_stop_btn = QPushButton("⏹ 停止录制")
|
||||
self.loot_record_stop_btn.clicked.connect(self.stop_loot_record)
|
||||
self.loot_record_stop_btn.setEnabled(False)
|
||||
loot_record_btn_layout.addWidget(self.loot_record_start_btn)
|
||||
loot_record_btn_layout.addWidget(self.loot_record_stop_btn)
|
||||
loot_record_btn_layout.addStretch()
|
||||
loot_record_inner.addLayout(loot_record_btn_layout)
|
||||
loot_record_layout.addWidget(loot_record_group)
|
||||
loot_record_layout.addStretch()
|
||||
tabs.addTab(tab_loot_record, "拾取录制")
|
||||
|
||||
# Tab: 参数配置(上下两个分组)
|
||||
tab_params = QWidget()
|
||||
params_layout = QVBoxLayout(tab_params)
|
||||
|
||||
# 基础参数(截图/窗口配置)
|
||||
basic_group = QGroupBox("基础参数")
|
||||
basic_layout = QHBoxLayout(basic_group)
|
||||
basic_left = QFormLayout()
|
||||
basic_right = QFormLayout()
|
||||
basic_grid = QGridLayout(basic_group)
|
||||
# 统一对齐配置
|
||||
LABEL_WIDTH = 100
|
||||
basic_grid.setColumnMinimumWidth(0, LABEL_WIDTH)
|
||||
basic_grid.setColumnMinimumWidth(2, LABEL_WIDTH)
|
||||
basic_grid.setColumnStretch(1, 1)
|
||||
basic_grid.setColumnStretch(3, 1)
|
||||
|
||||
self.gs_pixel_size = QSpinBox()
|
||||
self.gs_pixel_size.setRange(8, 32)
|
||||
self.gs_pixel_size.setValue(17)
|
||||
@@ -897,21 +996,33 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.gs_offset_top = QSpinBox()
|
||||
self.gs_offset_top.setRange(0, 100)
|
||||
self.gs_offset_top.setValue(45)
|
||||
basic_left.addRow("每格像素 (pixel_size):", self.gs_pixel_size)
|
||||
basic_left.addRow("起始 X (block_start_x):", self.gs_block_start_x)
|
||||
basic_left.addRow("截图宽度 (scan_region_width):", self.gs_scan_width)
|
||||
basic_right.addRow("截图高度 (scan_region_height):", self.gs_scan_height)
|
||||
basic_right.addRow("窗口左偏移 (offset_left):", self.gs_offset_left)
|
||||
basic_right.addRow("窗口顶偏移 (offset_top):", self.gs_offset_top)
|
||||
basic_layout.addLayout(basic_left)
|
||||
basic_layout.addLayout(basic_right)
|
||||
|
||||
# 第一列
|
||||
basic_grid.addWidget(QLabel("每格像素:"), 0, 0)
|
||||
basic_grid.addWidget(self.gs_pixel_size, 0, 1)
|
||||
basic_grid.addWidget(QLabel("起始 X:"), 1, 0)
|
||||
basic_grid.addWidget(self.gs_block_start_x, 1, 1)
|
||||
basic_grid.addWidget(QLabel("截图宽度:"), 2, 0)
|
||||
basic_grid.addWidget(self.gs_scan_width, 2, 1)
|
||||
# 第二列
|
||||
basic_grid.addWidget(QLabel("截图高度:"), 0, 2)
|
||||
basic_grid.addWidget(self.gs_scan_height, 0, 3)
|
||||
basic_grid.addWidget(QLabel("窗口左偏移:"), 1, 2)
|
||||
basic_grid.addWidget(self.gs_offset_left, 1, 3)
|
||||
basic_grid.addWidget(QLabel("窗口顶偏移:"), 2, 2)
|
||||
basic_grid.addWidget(self.gs_offset_top, 2, 3)
|
||||
|
||||
params_layout.addWidget(basic_group)
|
||||
|
||||
# 游戏参数
|
||||
game_group = QGroupBox("游戏参数")
|
||||
game_layout = QHBoxLayout(game_group)
|
||||
game_left = QFormLayout()
|
||||
game_right = QFormLayout()
|
||||
game_grid = QGridLayout(game_group)
|
||||
# 统一对齐配置
|
||||
game_grid.setColumnMinimumWidth(0, LABEL_WIDTH)
|
||||
game_grid.setColumnMinimumWidth(2, LABEL_WIDTH)
|
||||
game_grid.setColumnStretch(1, 1)
|
||||
game_grid.setColumnStretch(3, 1)
|
||||
|
||||
self.skinning_wait_spin = QDoubleSpinBox()
|
||||
self.skinning_wait_spin.setRange(0.1, 10.0)
|
||||
self.skinning_wait_spin.setSingleStep(0.1)
|
||||
@@ -960,20 +1071,39 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.gs_resurrect_key.setPlaceholderText("如 0")
|
||||
self.gs_resurrect_key.setMaxLength(16)
|
||||
self.gs_resurrect_key.setText("0")
|
||||
game_left.addRow("剥皮等待时间:", self.skinning_wait_spin)
|
||||
game_left.addRow("吃面包按键:", self.food_key_edit)
|
||||
game_left.addRow("吃面包血量阈值:", self.eat_hp_threshold_spin)
|
||||
game_left.addRow("吃面包最长等待:", self.eat_max_wait_spin)
|
||||
game_left.addRow("炉石按键:", self.gs_hearthstone_key)
|
||||
game_left.addRow("", self.gs_bag_full_hearthstone)
|
||||
game_right.addRow("是否上马:", self.gs_enable_mount)
|
||||
game_right.addRow("上马按键:", self.gs_mount_key)
|
||||
game_right.addRow("上马按住时长:", self.gs_mount_hold)
|
||||
game_right.addRow("上马重试间隔:", self.gs_mount_retry)
|
||||
game_right.addRow("释放灵魂按键:", self.gs_release_spirit_key)
|
||||
game_right.addRow("复活按键:", self.gs_resurrect_key)
|
||||
game_layout.addLayout(game_left)
|
||||
game_layout.addLayout(game_right)
|
||||
self.gs_enable_mouse_loot = QCheckBox("启用扫雷拾取")
|
||||
self.gs_enable_mouse_loot.setChecked(True)
|
||||
|
||||
# 网格填充
|
||||
game_grid.addWidget(QLabel("剥皮等待时间:"), 0, 0)
|
||||
game_grid.addWidget(self.skinning_wait_spin, 0, 1)
|
||||
game_grid.addWidget(self.gs_enable_mouse_loot, 0, 2)
|
||||
game_grid.addWidget(self.gs_enable_mount, 0, 3)
|
||||
|
||||
game_grid.addWidget(QLabel("吃面包按键:"), 1, 0)
|
||||
game_grid.addWidget(self.food_key_edit, 1, 1)
|
||||
game_grid.addWidget(QLabel("上马按键:"), 1, 2)
|
||||
game_grid.addWidget(self.gs_mount_key, 1, 3)
|
||||
|
||||
game_grid.addWidget(QLabel("吃面包血量阈值:"), 2, 0)
|
||||
game_grid.addWidget(self.eat_hp_threshold_spin, 2, 1)
|
||||
game_grid.addWidget(QLabel("上马按住时长:"), 2, 2)
|
||||
game_grid.addWidget(self.gs_mount_hold, 2, 3)
|
||||
|
||||
game_grid.addWidget(QLabel("吃面包最长等待:"), 3, 0)
|
||||
game_grid.addWidget(self.eat_max_wait_spin, 3, 1)
|
||||
game_grid.addWidget(QLabel("上马重试间隔:"), 3, 2)
|
||||
game_grid.addWidget(self.gs_mount_retry, 3, 3)
|
||||
|
||||
game_grid.addWidget(QLabel("炉石按键:"), 4, 0)
|
||||
game_grid.addWidget(self.gs_hearthstone_key, 4, 1)
|
||||
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(QLabel("复活按键:"), 5, 2)
|
||||
game_grid.addWidget(self.gs_resurrect_key, 5, 3)
|
||||
|
||||
params_layout.addWidget(game_group)
|
||||
|
||||
params_layout.addStretch()
|
||||
@@ -1025,11 +1155,13 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.food_key_edit.setText(str(bot_cfg.get('food_key', 'f1')).strip() or 'f1')
|
||||
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)))
|
||||
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)
|
||||
|
||||
def _save_params_config(self):
|
||||
"""保存「参数配置」界面到 game_state_config.json(多分辨率)并写入 wow_multikey_qt.json(bot 参数)"""
|
||||
@@ -1058,6 +1190,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.config['bot']['food_key'] = self.food_key_edit.text().strip() or 'f1'
|
||||
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._save_main_config()
|
||||
|
||||
self.log(f"✅ 参数配置已保存至 {path},并更新 bot 参数")
|
||||
@@ -1505,6 +1638,8 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
release_spirit_key = '9'
|
||||
resurrect_key = '0'
|
||||
|
||||
enable_mouse_loot = self.gs_enable_mouse_loot.isChecked()
|
||||
|
||||
self.game_worker = GameLoopWorker(
|
||||
mode, waypoints_path=waypoints_path, vendor_path=vendor_path,
|
||||
attack_loop_path=attack_loop_path,
|
||||
@@ -1523,6 +1658,7 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
resurrection_waypoints_path=resurrection_waypoints_path,
|
||||
release_spirit_key=release_spirit_key,
|
||||
resurrect_key=resurrect_key,
|
||||
enable_mouse_loot=enable_mouse_loot,
|
||||
)
|
||||
self.game_worker.state_signal.connect(self.state_label.setText)
|
||||
self.game_worker.log_signal.connect(self.log)
|
||||
@@ -1625,6 +1761,36 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
else:
|
||||
QMessageBox.warning(self, "提示", "请先进行巡逻点录制")
|
||||
|
||||
def start_loot_record(self):
|
||||
"""开始录制拾取轨迹。"""
|
||||
if not self.hwnd:
|
||||
QMessageBox.warning(self, "提示", "未找到游戏窗口,无法录制轨迹")
|
||||
return
|
||||
if self.loot_recorder_worker and self.loot_recorder_worker.isRunning():
|
||||
return
|
||||
|
||||
self.loot_recorder_worker = LootPathRecorderWorker(self.hwnd)
|
||||
self.loot_recorder_worker.log_signal.connect(self.log)
|
||||
self.loot_recorder_worker.finished_signal.connect(self._on_loot_record_finished)
|
||||
self.loot_recorder_worker.start()
|
||||
|
||||
self.loot_record_start_btn.setEnabled(False)
|
||||
self.loot_record_stop_btn.setEnabled(True)
|
||||
self.status_bar.showMessage("🔴 拾取轨迹录制中...")
|
||||
|
||||
def stop_loot_record(self):
|
||||
"""停止录制拾取轨迹。"""
|
||||
if self.loot_recorder_worker:
|
||||
self.loot_recorder_worker.stop()
|
||||
self.loot_record_start_btn.setEnabled(True)
|
||||
self.loot_record_stop_btn.setEnabled(False)
|
||||
self.status_bar.showMessage("✅ 录制已结束")
|
||||
|
||||
def _on_loot_record_finished(self, count):
|
||||
self.log(f"🎬 拾取录制完成,共记录 {count} 个有效点")
|
||||
self.loot_record_start_btn.setEnabled(True)
|
||||
self.loot_record_stop_btn.setEnabled(False)
|
||||
|
||||
def closeEvent(self, event):
|
||||
running = bool(self.key_workers) or (self.game_worker and self.game_worker.isRunning())
|
||||
if running:
|
||||
|
||||
Reference in New Issue
Block a user