更新:添加鼠标图标识别、复活逻辑优化、参数配置加载修复、目标血量100%检测
BIN
__pycache__/build.cpython-311.pyc
Normal file
BIN
__pycache__/build_wow_multikey.cpython-311.pyc
Normal file
BIN
__pycache__/hardware_control.cpython-311.pyc
Normal file
275
auto_bot.py
@@ -8,7 +8,6 @@ import ctypes
|
||||
import cv2
|
||||
import numpy as np
|
||||
import win32gui
|
||||
import win32api
|
||||
import win32con
|
||||
|
||||
# 开启 DPI 意识
|
||||
@@ -18,7 +17,7 @@ except Exception:
|
||||
ctypes.windll.user32.SetProcessDPIAware()
|
||||
|
||||
from hardware_control import hw_ctrl
|
||||
from game_state import parse_game_state
|
||||
from game_state import parse_game_state, load_layout_config
|
||||
from stuck_handler import StuckHandler
|
||||
|
||||
# 定义按键常量
|
||||
@@ -32,16 +31,44 @@ def _config_base():
|
||||
return os.path.dirname(sys.executable)
|
||||
return os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
|
||||
def get_config_path(filename):
|
||||
base = _config_base()
|
||||
p = os.path.join(base, filename)
|
||||
if os.path.exists(p):
|
||||
return p
|
||||
if getattr(sys, 'frozen', False):
|
||||
meipass = getattr(sys, '_MEIPASS', '')
|
||||
if meipass:
|
||||
p2 = os.path.join(meipass, filename)
|
||||
if os.path.exists(p2):
|
||||
return p2
|
||||
return p
|
||||
|
||||
|
||||
def move_cursor_hw(x, y, settle_sec=0.02):
|
||||
hw_ctrl.move_to(int(x), int(y))
|
||||
if settle_sec > 0:
|
||||
time.sleep(settle_sec)
|
||||
|
||||
|
||||
def get_wow_client_rect(hwnd):
|
||||
client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd)
|
||||
screen_left, screen_top = win32gui.ClientToScreen(hwnd, (client_left, client_top))
|
||||
screen_right, screen_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom))
|
||||
return screen_left, screen_top, screen_right, screen_bottom
|
||||
|
||||
class CursorManager:
|
||||
"""通过图像识别判断鼠标图标类型"""
|
||||
def __init__(self):
|
||||
self.templates = {}
|
||||
self.handle_cache = {}
|
||||
self.log_path = get_config_path('cursor_recognition.log')
|
||||
self._load_templates()
|
||||
|
||||
def _load_templates(self):
|
||||
# 强制使用纯相对路径,由 Python 自动处理 CWD,完美避开中文路径编码问题
|
||||
cursor_dir = os.path.join('images', 'cursor')
|
||||
cursor_dir = get_config_path(os.path.join('images', 'cursor'))
|
||||
files = {'Point': 'Point.PNG', 'Attack': 'Attack.PNG', 'LootAll': 'LootAll.PNG', 'Skin': 'Skin.PNG'}
|
||||
for name, fname in files.items():
|
||||
path = os.path.join(cursor_dir, fname)
|
||||
@@ -66,6 +93,33 @@ class CursorManager:
|
||||
self.handle_cache[hcursor] = res
|
||||
return res
|
||||
|
||||
def _debug_log(self, message):
|
||||
try:
|
||||
with open(self.log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _normalize_for_match(self, img):
|
||||
if img is None:
|
||||
return None
|
||||
if len(img.shape) == 2:
|
||||
gray = img
|
||||
elif img.shape[2] == 4:
|
||||
alpha = img[:, :, 3].astype(np.float32) / 255.0
|
||||
bgr = img[:, :, :3].astype(np.float32)
|
||||
bg = np.full_like(bgr, 255.0)
|
||||
composed = (bgr * alpha[..., None]) + (bg * (1.0 - alpha[..., None]))
|
||||
gray = cv2.cvtColor(composed.astype(np.uint8), cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY)
|
||||
return cv2.GaussianBlur(gray, (3, 3), 0)
|
||||
|
||||
def _edge_map(self, gray):
|
||||
if gray is None:
|
||||
return None
|
||||
return cv2.Canny(gray, 32, 96)
|
||||
|
||||
def _identify(self, hcursor):
|
||||
import win32ui
|
||||
try:
|
||||
@@ -85,20 +139,37 @@ class CursorManager:
|
||||
best_name = 'Other'
|
||||
max_score = 0.0
|
||||
|
||||
scales = [0.8, 1.0, 1.2]
|
||||
best_scale = 1.0
|
||||
target_gray = self._normalize_for_match(target)
|
||||
target_edge = self._edge_map(target_gray)
|
||||
target_has_edge = target_edge is not None and np.count_nonzero(target_edge) > 0
|
||||
scales = [0.55, 0.65, 0.75, 0.85, 0.95, 1.0, 1.1, 1.2, 1.35, 1.5]
|
||||
for name, temp in self.templates.items():
|
||||
t_h, t_w = temp.shape[:2]
|
||||
for s in scales:
|
||||
s_w, s_h = int(t_w * s), int(t_h * s)
|
||||
if s_w > width or s_h > height: continue
|
||||
res_temp = cv2.resize(temp, (s_w, s_h))
|
||||
res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED)
|
||||
_, score, _, _ = cv2.minMaxLoc(res)
|
||||
if s_w > width or s_h > height:
|
||||
continue
|
||||
interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC
|
||||
res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp)
|
||||
temp_gray = self._normalize_for_match(res_temp)
|
||||
if temp_gray is None:
|
||||
continue
|
||||
gray_res = cv2.matchTemplate(target_gray, temp_gray, cv2.TM_CCOEFF_NORMED)
|
||||
_, gray_score, _, _ = cv2.minMaxLoc(gray_res)
|
||||
score = gray_score
|
||||
temp_edge = self._edge_map(temp_gray)
|
||||
if target_has_edge and temp_edge is not None and np.count_nonzero(temp_edge) > 0:
|
||||
edge_res = cv2.matchTemplate(target_edge, temp_edge, cv2.TM_CCOEFF_NORMED)
|
||||
_, edge_score, _, _ = cv2.minMaxLoc(edge_res)
|
||||
score = max(score, edge_score)
|
||||
if score > max_score:
|
||||
max_score = score
|
||||
best_name = name
|
||||
best_scale = s
|
||||
|
||||
if max_score > 0.2:
|
||||
self._debug_log(f"best={best_name} score={max_score:.3f} scale={best_scale:.2f}")
|
||||
print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}")
|
||||
return best_name, max_score
|
||||
except Exception:
|
||||
@@ -143,6 +214,11 @@ class AutoBot:
|
||||
self.skinning_wait_sec = float(skinning_wait_sec) if skinning_wait_sec is not None else 1.5
|
||||
self.enable_mouse_loot = enable_mouse_loot
|
||||
self.cursor_mgr = CursorManager()
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self.attack_stall_scan_threshold = 2.0
|
||||
self.attack_scan_retry_sec = 2.0
|
||||
self._last_mouse_path_scale_signature = None
|
||||
|
||||
def execute_disengage_loot(self):
|
||||
"""从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。"""
|
||||
@@ -159,8 +235,160 @@ class AutoBot:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _load_mouse_path_points(self, client_width, client_height):
|
||||
path_points = []
|
||||
layout = load_layout_config()
|
||||
base_width = int(layout.get('mouse_path_base_window_width', 2560) or 2560)
|
||||
base_height = int(layout.get('mouse_path_base_window_height', 1600) or 1600)
|
||||
path_file = get_config_path("loot_path.json")
|
||||
if os.path.exists(path_file):
|
||||
with open(path_file, 'r', encoding='utf-8') as f:
|
||||
raw_path = json.load(f)
|
||||
|
||||
if isinstance(raw_path, dict):
|
||||
path_points = raw_path.get('points') or raw_path.get('path_points') or raw_path.get('offsets') or []
|
||||
base_width = int(
|
||||
raw_path.get('base_window_width')
|
||||
or raw_path.get('window_width')
|
||||
or raw_path.get('base_client_width')
|
||||
or base_width
|
||||
)
|
||||
base_height = int(
|
||||
raw_path.get('base_window_height')
|
||||
or raw_path.get('window_height')
|
||||
or raw_path.get('base_client_height')
|
||||
or base_height
|
||||
)
|
||||
else:
|
||||
path_points = raw_path
|
||||
|
||||
if path_points:
|
||||
scale_x = client_width / max(base_width, 1)
|
||||
scale_y = client_height / max(base_height, 1)
|
||||
signature = (client_width, client_height, base_width, base_height)
|
||||
if signature != self._last_mouse_path_scale_signature:
|
||||
print(
|
||||
f">>> [扫雷路径] 当前窗口 {client_width}x{client_height},"
|
||||
f"基准 {base_width}x{base_height},缩放 x={scale_x:.3f} y={scale_y:.3f}"
|
||||
)
|
||||
self._last_mouse_path_scale_signature = signature
|
||||
|
||||
scaled_points = []
|
||||
for point in path_points:
|
||||
if not isinstance(point, (list, tuple)) or len(point) < 2:
|
||||
continue
|
||||
dx = int(round(float(point[0]) * scale_x))
|
||||
dy = int(round(float(point[1]) * scale_y))
|
||||
scaled_points.append((dx, dy))
|
||||
if scaled_points:
|
||||
return scaled_points
|
||||
|
||||
x_scale, y_scale = 1.8, 0.8
|
||||
for r in range(50, client_height // 2, 40):
|
||||
angles = range(180, 360, 5) if (r // 40) % 2 == 0 else range(360, 180, -5)
|
||||
for a in angles:
|
||||
rad = math.radians(a)
|
||||
path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale)))
|
||||
return path_points
|
||||
|
||||
def mouse_sweep_scan(
|
||||
self,
|
||||
target_cursor_types,
|
||||
click_wait_map=None,
|
||||
max_scan_sec=15.0,
|
||||
score_threshold=0.7,
|
||||
return_on_first_click=False,
|
||||
):
|
||||
if random.random() < 0.1:
|
||||
return False
|
||||
hwnd = win32gui.FindWindow(None, WIN_TITLE)
|
||||
if not hwnd:
|
||||
return False
|
||||
|
||||
click_wait_map = click_wait_map or {}
|
||||
target_cursor_types = set(target_cursor_types or [])
|
||||
|
||||
try:
|
||||
left, top, right, bottom = get_wow_client_rect(hwnd)
|
||||
client_width = max(right - left, 1)
|
||||
client_height = max(bottom - top, 1)
|
||||
center_x = left + (right - left) // 2
|
||||
center_y = top + (bottom - top) // 2
|
||||
|
||||
move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
|
||||
_, default_hcursor, _ = win32gui.GetCursorInfo()
|
||||
|
||||
path_points = self._load_mouse_path_points(client_width, client_height)
|
||||
start_time = time.time()
|
||||
clicked_positions = []
|
||||
|
||||
for dx, dy in path_points:
|
||||
if time.time() - start_time > max_scan_sec:
|
||||
break
|
||||
|
||||
target_x = center_x + dx + random.randint(-5, 5)
|
||||
target_y = center_y + dy + random.randint(-5, 5)
|
||||
|
||||
if not (left + 10 < target_x < right - 10 and top + 10 < target_y < bottom - 10):
|
||||
continue
|
||||
if any(math.dist((target_x, target_y), pos) < 30 for pos in clicked_positions):
|
||||
continue
|
||||
|
||||
move_cursor_hw(target_x, target_y, settle_sec=0.02)
|
||||
|
||||
_, hcursor, _ = win32gui.GetCursorInfo()
|
||||
if hcursor == 0 or hcursor == default_hcursor:
|
||||
continue
|
||||
|
||||
ctype_name, score = self.cursor_mgr.get_type(hcursor)
|
||||
if score > score_threshold and ctype_name in target_cursor_types:
|
||||
print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击")
|
||||
hw_ctrl.right_click()
|
||||
clicked_positions.append((target_x, target_y))
|
||||
|
||||
wait_sec = float(click_wait_map.get(ctype_name, 0.3))
|
||||
if wait_sec > 0:
|
||||
time.sleep(wait_sec)
|
||||
|
||||
if return_on_first_click:
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return True
|
||||
|
||||
wait_start = time.time()
|
||||
while time.time() - wait_start < 0.8:
|
||||
_, curr_h, _ = win32gui.GetCursorInfo()
|
||||
check_name, _ = self.cursor_mgr.get_type(curr_h)
|
||||
if check_name == 'Point':
|
||||
break
|
||||
time.sleep(0.1)
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
elif score > 0.4:
|
||||
print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 阈值 {score_threshold})")
|
||||
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return bool(clicked_positions)
|
||||
except Exception as e:
|
||||
print(f">>> [扫雷扫描] 出错: {e}")
|
||||
return False
|
||||
|
||||
def mouse_scan_attack_target(self):
|
||||
return self.mouse_sweep_scan(
|
||||
['Attack'],
|
||||
click_wait_map={'Attack': 0.3},
|
||||
max_scan_sec=4.0,
|
||||
score_threshold=0.6,
|
||||
return_on_first_click=True,
|
||||
)
|
||||
|
||||
def mouse_sweep_loot(self):
|
||||
"""支持图标识别的高精度扫雷拾取。"""
|
||||
return self.mouse_sweep_scan(
|
||||
['LootAll', 'Skin'],
|
||||
click_wait_map={'LootAll': 1.3, 'Skin': self.skinning_wait_sec},
|
||||
max_scan_sec=15.0,
|
||||
score_threshold=0.7,
|
||||
return_on_first_click=False,
|
||||
)
|
||||
if random.random() < 0.1: return False
|
||||
hwnd = win32gui.FindWindow(None, WIN_TITLE)
|
||||
if not hwnd: return False
|
||||
@@ -172,13 +400,12 @@ class AutoBot:
|
||||
center_y = top + (bottom - top) // 2
|
||||
|
||||
# 1. 强制“角落校准”采样
|
||||
win32api.SetCursorPos((left + 50, top + 50))
|
||||
time.sleep(0.2)
|
||||
move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
|
||||
_, default_hcursor, _ = win32gui.GetCursorInfo()
|
||||
|
||||
# 2. 获取扫瞄路径点位
|
||||
path_points = []
|
||||
path_file = "loot_path.json"
|
||||
path_file = get_config_path("loot_path.json")
|
||||
if os.path.exists(path_file):
|
||||
with open(path_file, 'r') as f:
|
||||
path_points = json.load(f)
|
||||
@@ -204,8 +431,7 @@ class AutoBot:
|
||||
if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue
|
||||
if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue
|
||||
|
||||
win32api.SetCursorPos((target_x, target_y))
|
||||
time.sleep(0.02)
|
||||
move_cursor_hw(target_x, target_y, settle_sec=0.02)
|
||||
|
||||
_, hcursor, _ = win32gui.GetCursorInfo()
|
||||
if hcursor != 0 and hcursor != default_hcursor:
|
||||
@@ -231,7 +457,7 @@ class AutoBot:
|
||||
elif score > 0.4:
|
||||
print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 门槛 0.7)")
|
||||
|
||||
win32api.SetCursorPos((center_x, center_y))
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f">>> [扫雷拾取] 出错: {e}")
|
||||
@@ -288,10 +514,19 @@ class AutoBot:
|
||||
self._has_braked_for_target = True
|
||||
|
||||
# 2. 交互逻辑
|
||||
cooldown = 2.0 if not state['combat'] else 6.0
|
||||
if is_new_target or (current_time - self.last_interaction_time > cooldown):
|
||||
hw_ctrl.press(KEY_LOOT)
|
||||
self.last_interaction_time = current_time
|
||||
hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp
|
||||
if is_new_target or hp_dropped or self.last_target_damage_time is None:
|
||||
self.last_target_damage_time = current_time
|
||||
|
||||
if (
|
||||
state['combat']
|
||||
and self.last_target_damage_time is not None
|
||||
and (current_time - self.last_target_damage_time) >= self.attack_stall_scan_threshold
|
||||
and (current_time - self.last_attack_scan_time) >= self.attack_scan_retry_sec
|
||||
):
|
||||
if self.mouse_scan_attack_target():
|
||||
self.last_target_damage_time = current_time
|
||||
self.last_attack_scan_time = current_time
|
||||
self.last_target_hp = target_hp
|
||||
if state['combat']:
|
||||
self.execute_combat_logic(state)
|
||||
@@ -304,11 +539,15 @@ class AutoBot:
|
||||
self._was_in_combat_or_target = False
|
||||
self.last_tab_time = current_time + 1.0
|
||||
self.last_target_hp = 0
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self._has_braked_for_target = False
|
||||
return
|
||||
|
||||
self._was_in_combat_or_target = False
|
||||
self.last_target_hp = 0
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self._has_braked_for_target = False
|
||||
|
||||
self.tab_no_target_count = min(self.tab_no_target_count, 5)
|
||||
|
||||
290
auto_bot_move.py
@@ -7,6 +7,8 @@ import math
|
||||
import ctypes
|
||||
import cv2
|
||||
import numpy as np
|
||||
import win32con
|
||||
import win32gui
|
||||
from hardware_control import hw_ctrl
|
||||
from game_state import parse_game_state, load_layout_config
|
||||
from coordinate_patrol import CoordinatePatrol
|
||||
@@ -28,6 +30,11 @@ WIN_TITLE = "魔兽世界"
|
||||
# 默认巡逻航点(waypoints.json 不存在或无效时使用)
|
||||
DEFAULT_WAYPOINTS = [(23.8, 71.0), (27.0, 79.0), (31.0, 72.0)]
|
||||
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(1)
|
||||
except Exception:
|
||||
ctypes.windll.user32.SetProcessDPIAware()
|
||||
|
||||
|
||||
def _config_base():
|
||||
"""打包成 exe 时,优先从 exe 同目录读配置,否则用内嵌资源目录。"""
|
||||
@@ -54,6 +61,19 @@ def get_config_path(filename):
|
||||
return os.path.join(base, filename)
|
||||
|
||||
|
||||
def move_cursor_hw(x, y, settle_sec=0.02):
|
||||
hw_ctrl.move_to(int(x), int(y))
|
||||
if settle_sec > 0:
|
||||
time.sleep(settle_sec)
|
||||
|
||||
|
||||
def get_wow_client_rect(hwnd):
|
||||
client_left, client_top, client_right, client_bottom = win32gui.GetClientRect(hwnd)
|
||||
screen_left, screen_top = win32gui.ClientToScreen(hwnd, (client_left, client_top))
|
||||
screen_right, screen_bottom = win32gui.ClientToScreen(hwnd, (client_right, client_bottom))
|
||||
return screen_left, screen_top, screen_right, screen_bottom
|
||||
|
||||
|
||||
def load_waypoints(path=None):
|
||||
path = path or get_config_path(WAYPOINTS_FILE)
|
||||
if os.path.exists(path):
|
||||
@@ -90,11 +110,12 @@ class CursorManager:
|
||||
def __init__(self):
|
||||
self.templates = {}
|
||||
self.handle_cache = {} # 句柄 -> 类型缓存
|
||||
self.log_path = get_config_path('cursor_recognition.log')
|
||||
self._load_templates()
|
||||
|
||||
def _load_templates(self):
|
||||
# 强制使用纯相对路径,不带盘符前缀,由 Python 自动处理 CWD
|
||||
cursor_dir = os.path.join('images', 'cursor')
|
||||
cursor_dir = get_config_path(os.path.join('images', 'cursor'))
|
||||
files = {
|
||||
'Point': 'Point.PNG',
|
||||
'Attack': 'Attack.PNG',
|
||||
@@ -125,6 +146,33 @@ class CursorManager:
|
||||
self.handle_cache[hcursor] = res
|
||||
return res
|
||||
|
||||
def _debug_log(self, message):
|
||||
try:
|
||||
with open(self.log_path, 'a', encoding='utf-8') as f:
|
||||
f.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {message}\n")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _normalize_for_match(self, img):
|
||||
if img is None:
|
||||
return None
|
||||
if len(img.shape) == 2:
|
||||
gray = img
|
||||
elif img.shape[2] == 4:
|
||||
alpha = img[:, :, 3].astype(np.float32) / 255.0
|
||||
bgr = img[:, :, :3].astype(np.float32)
|
||||
bg = np.full_like(bgr, 255.0)
|
||||
composed = (bgr * alpha[..., None]) + (bg * (1.0 - alpha[..., None]))
|
||||
gray = cv2.cvtColor(composed.astype(np.uint8), cv2.COLOR_BGR2GRAY)
|
||||
else:
|
||||
gray = cv2.cvtColor(img[:, :, :3], cv2.COLOR_BGR2GRAY)
|
||||
return cv2.GaussianBlur(gray, (3, 3), 0)
|
||||
|
||||
def _edge_map(self, gray):
|
||||
if gray is None:
|
||||
return None
|
||||
return cv2.Canny(gray, 32, 96)
|
||||
|
||||
def _identify(self, hcursor):
|
||||
import win32ui
|
||||
try:
|
||||
@@ -146,21 +194,40 @@ class CursorManager:
|
||||
max_score = 0.0
|
||||
|
||||
# 多尺度匹配:尝试 0.8, 1.0, 1.2 倍缩放,解决 UI 缩放问题
|
||||
scales = [0.8, 1.0, 1.2]
|
||||
best_scale = 1.0
|
||||
target_gray = self._normalize_for_match(target)
|
||||
target_edge = self._edge_map(target_gray)
|
||||
target_has_edge = target_edge is not None and np.count_nonzero(target_edge) > 0
|
||||
scales = [0.55, 0.65, 0.75, 0.85, 0.95, 1.0, 1.1, 1.2, 1.35, 1.5]
|
||||
for name, temp in self.templates.items():
|
||||
t_h, t_w = temp.shape[:2]
|
||||
for s in scales:
|
||||
s_w, s_h = int(t_w * s), int(t_h * s)
|
||||
if s_w > width or s_h > height: continue
|
||||
|
||||
res_temp = cv2.resize(temp, (s_w, s_h))
|
||||
res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED)
|
||||
_, score, _, _ = cv2.minMaxLoc(res)
|
||||
if s_w > width or s_h > height:
|
||||
continue
|
||||
|
||||
interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC
|
||||
res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp)
|
||||
temp_gray = self._normalize_for_match(res_temp)
|
||||
if temp_gray is None:
|
||||
continue
|
||||
|
||||
gray_res = cv2.matchTemplate(target_gray, temp_gray, cv2.TM_CCOEFF_NORMED)
|
||||
_, gray_score, _, _ = cv2.minMaxLoc(gray_res)
|
||||
score = gray_score
|
||||
|
||||
temp_edge = self._edge_map(temp_gray)
|
||||
if target_has_edge and temp_edge is not None and np.count_nonzero(temp_edge) > 0:
|
||||
edge_res = cv2.matchTemplate(target_edge, temp_edge, cv2.TM_CCOEFF_NORMED)
|
||||
_, edge_score, _, _ = cv2.minMaxLoc(edge_res)
|
||||
score = max(score, edge_score)
|
||||
if score > max_score:
|
||||
max_score = score
|
||||
best_name = name
|
||||
best_scale = s
|
||||
|
||||
if max_score > 0.2:
|
||||
self._debug_log(f"best={best_name} score={max_score:.3f} scale={best_scale:.2f}")
|
||||
print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}")
|
||||
return best_name, max_score
|
||||
except Exception as e:
|
||||
@@ -202,6 +269,11 @@ class AutoBotMove:
|
||||
self.attack_loop_config = load_attack_loop(attack_loop_path)
|
||||
self._prev_death_state = 0
|
||||
self._eating_started_at = None
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self.attack_stall_scan_threshold = 2.0
|
||||
self.attack_scan_retry_sec = 2.0
|
||||
self._last_mouse_path_scale_signature = None
|
||||
# stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航)
|
||||
self._stop_check = stop_check if callable(stop_check) else (lambda: False)
|
||||
if waypoints is None:
|
||||
@@ -259,16 +331,173 @@ class AutoBotMove:
|
||||
else:
|
||||
# 关闭扫雷时,执行基础的交互拾取
|
||||
hw_ctrl.press(KEY_LOOT)
|
||||
time.sleep(0.5)
|
||||
time.sleep(0.8)
|
||||
|
||||
# 2. 最后补漏剥皮(针对脚下尸体)
|
||||
# hw_ctrl.press(KEY_LOOT)
|
||||
# time.sleep(self.skinning_wait_sec + 0.5)
|
||||
hw_ctrl.press(KEY_LOOT)
|
||||
time.sleep(self.skinning_wait_sec + 0.5)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _load_mouse_path_points(self, client_width, client_height):
|
||||
path_points = []
|
||||
layout = load_layout_config()
|
||||
base_width = int(layout.get('mouse_path_base_window_width', 2560) or 2560)
|
||||
base_height = int(layout.get('mouse_path_base_window_height', 1600) or 1600)
|
||||
path_file = get_config_path("loot_path.json")
|
||||
if os.path.exists(path_file):
|
||||
with open(path_file, 'r', encoding='utf-8') as f:
|
||||
raw_path = json.load(f)
|
||||
|
||||
if isinstance(raw_path, dict):
|
||||
path_points = raw_path.get('points') or raw_path.get('path_points') or raw_path.get('offsets') or []
|
||||
base_width = int(
|
||||
raw_path.get('base_window_width')
|
||||
or raw_path.get('window_width')
|
||||
or raw_path.get('base_client_width')
|
||||
or base_width
|
||||
)
|
||||
base_height = int(
|
||||
raw_path.get('base_window_height')
|
||||
or raw_path.get('window_height')
|
||||
or raw_path.get('base_client_height')
|
||||
or base_height
|
||||
)
|
||||
else:
|
||||
path_points = raw_path
|
||||
|
||||
if path_points:
|
||||
scale_x = client_width / max(base_width, 1)
|
||||
scale_y = client_height / max(base_height, 1)
|
||||
signature = (client_width, client_height, base_width, base_height)
|
||||
if signature != self._last_mouse_path_scale_signature:
|
||||
print(
|
||||
f">>> [扫雷路径] 当前窗口 {client_width}x{client_height},"
|
||||
f"基准 {base_width}x{base_height},缩放 x={scale_x:.3f} y={scale_y:.3f}"
|
||||
)
|
||||
self._last_mouse_path_scale_signature = signature
|
||||
|
||||
scaled_points = []
|
||||
for point in path_points:
|
||||
if not isinstance(point, (list, tuple)) or len(point) < 2:
|
||||
continue
|
||||
dx = int(round(float(point[0]) * scale_x))
|
||||
dy = int(round(float(point[1]) * scale_y))
|
||||
scaled_points.append((dx, dy))
|
||||
if scaled_points:
|
||||
return scaled_points
|
||||
|
||||
x_scale, y_scale = 1.8, 0.8
|
||||
for r in range(50, client_height // 2, 40):
|
||||
angles = range(180, 360, 5) if (r // 40) % 2 == 0 else range(360, 180, -5)
|
||||
for a in angles:
|
||||
rad = math.radians(a)
|
||||
path_points.append((int(r * math.cos(rad) * x_scale), int(r * math.sin(rad) * y_scale)))
|
||||
return path_points
|
||||
|
||||
def mouse_sweep_scan(
|
||||
self,
|
||||
target_cursor_types,
|
||||
click_wait_map=None,
|
||||
max_scan_sec=15.0,
|
||||
score_threshold=0.7,
|
||||
return_on_first_click=False,
|
||||
):
|
||||
if random.random() < 0.1:
|
||||
return False
|
||||
hwnd = win32gui.FindWindow(None, WIN_TITLE)
|
||||
if not hwnd:
|
||||
return False
|
||||
|
||||
click_wait_map = click_wait_map or {}
|
||||
target_cursor_types = set(target_cursor_types or [])
|
||||
|
||||
try:
|
||||
left, top, right, bottom = get_wow_client_rect(hwnd)
|
||||
client_width = max(right - left, 1)
|
||||
client_height = max(bottom - top, 1)
|
||||
center_x = left + (right - left) // 2
|
||||
center_y = top + (bottom - top) // 2
|
||||
|
||||
move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
|
||||
_, default_hcursor, _ = win32gui.GetCursorInfo()
|
||||
|
||||
path_points = self._load_mouse_path_points(client_width, client_height)
|
||||
start_time = time.time()
|
||||
clicked_positions = []
|
||||
|
||||
for dx, dy in path_points:
|
||||
if time.time() - start_time > max_scan_sec:
|
||||
break
|
||||
|
||||
target_x = center_x + dx + random.randint(-5, 5)
|
||||
target_y = center_y + dy + random.randint(-5, 5)
|
||||
|
||||
if not (left + 10 < target_x < right - 10 and top + 10 < target_y < bottom - 10):
|
||||
continue
|
||||
if any(math.dist((target_x, target_y), pos) < 30 for pos in clicked_positions):
|
||||
continue
|
||||
|
||||
move_cursor_hw(target_x, target_y, settle_sec=0.02)
|
||||
|
||||
_, hcursor, _ = win32gui.GetCursorInfo()
|
||||
if hcursor == 0 or hcursor == default_hcursor:
|
||||
if self._should_stop():
|
||||
break
|
||||
continue
|
||||
|
||||
ctype_name, score = self.cursor_mgr.get_type(hcursor)
|
||||
if score > score_threshold and ctype_name in target_cursor_types:
|
||||
print(f">>> [扫雷] 识别成功: {ctype_name} (得分: {score:.3f}), 执行右键点击")
|
||||
hw_ctrl.right_click()
|
||||
clicked_positions.append((target_x, target_y))
|
||||
|
||||
wait_sec = float(click_wait_map.get(ctype_name, 0.3))
|
||||
if wait_sec > 0:
|
||||
time.sleep(wait_sec)
|
||||
|
||||
if return_on_first_click:
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return True
|
||||
|
||||
wait_start = time.time()
|
||||
while time.time() - wait_start < 0.8:
|
||||
_, curr_h, _ = win32gui.GetCursorInfo()
|
||||
check_name, _ = self.cursor_mgr.get_type(curr_h)
|
||||
if check_name == 'Point':
|
||||
break
|
||||
time.sleep(0.1)
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
elif score > 0.4:
|
||||
print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 阈值 {score_threshold})")
|
||||
|
||||
if self._should_stop():
|
||||
break
|
||||
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return bool(clicked_positions)
|
||||
except Exception as e:
|
||||
print(f">>> [扫雷扫描] 出错: {e}")
|
||||
return False
|
||||
|
||||
def mouse_scan_attack_target(self):
|
||||
return self.mouse_sweep_scan(
|
||||
['Attack'],
|
||||
click_wait_map={'Attack': 0.3},
|
||||
max_scan_sec=4.0,
|
||||
score_threshold=0.6,
|
||||
return_on_first_click=True,
|
||||
)
|
||||
|
||||
def mouse_sweep_loot(self):
|
||||
"""支持图标识别的高精度扫雷拾取。"""
|
||||
return self.mouse_sweep_scan(
|
||||
['LootAll', 'Skin'],
|
||||
click_wait_map={'LootAll': 1.3, 'Skin': self.skinning_wait_sec},
|
||||
max_scan_sec=15.0,
|
||||
score_threshold=0.7,
|
||||
return_on_first_click=False,
|
||||
)
|
||||
if random.random() < 0.1: return False
|
||||
hwnd = win32gui.FindWindow(None, WIN_TITLE)
|
||||
if not hwnd: return False
|
||||
@@ -280,8 +509,7 @@ class AutoBotMove:
|
||||
center_y = top + (bottom - top) // 2
|
||||
|
||||
# 1. 强制“角落校准”采样
|
||||
win32api.SetCursorPos((left + 50, top + 50))
|
||||
time.sleep(0.2)
|
||||
move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
|
||||
_, default_hcursor, _ = win32gui.GetCursorInfo()
|
||||
|
||||
# 2. 获取扫瞄路径点位
|
||||
@@ -312,8 +540,7 @@ class AutoBotMove:
|
||||
if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue
|
||||
if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue
|
||||
|
||||
win32api.SetCursorPos((target_x, target_y))
|
||||
time.sleep(0.02)
|
||||
move_cursor_hw(target_x, target_y, settle_sec=0.02)
|
||||
|
||||
_, hcursor, _ = win32gui.GetCursorInfo()
|
||||
if hcursor != 0 and hcursor != default_hcursor:
|
||||
@@ -343,7 +570,7 @@ class AutoBotMove:
|
||||
|
||||
if self._should_stop(): break
|
||||
|
||||
win32api.SetCursorPos((center_x, center_y))
|
||||
move_cursor_hw(center_x, center_y, settle_sec=0.02)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f">>> [扫雷拾取] 出错: {e}")
|
||||
@@ -472,17 +699,38 @@ class AutoBotMove:
|
||||
self._has_braked_for_target = True
|
||||
|
||||
# 2. 交互键(KEY_LOOT)按键策略:
|
||||
cooldown = 2.0 if not state['combat'] else 6.0
|
||||
if is_new_target or (now - self.last_interaction_time > cooldown):
|
||||
hw_ctrl.press(KEY_LOOT)
|
||||
self.last_interaction_time = now
|
||||
hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp
|
||||
if is_new_target or hp_dropped or self.last_target_damage_time is None:
|
||||
self.last_target_damage_time = now
|
||||
|
||||
# 目标血量100%且超过2秒没掉血,按交互键尝试选中/攻击
|
||||
if target_hp >= 100 and self.last_target_damage_time is not None:
|
||||
if (now - self.last_target_damage_time) >= 2.0:
|
||||
hw_ctrl.press(KEY_LOOT)
|
||||
self.last_target_damage_time = now
|
||||
|
||||
if (
|
||||
state['combat']
|
||||
and self.last_target_damage_time is not None
|
||||
and (now - self.last_target_damage_time) >= self.attack_stall_scan_threshold
|
||||
and (now - self.last_attack_scan_time) >= self.attack_scan_retry_sec
|
||||
):
|
||||
if self.mouse_scan_attack_target():
|
||||
self.last_target_damage_time = now
|
||||
self.last_attack_scan_time = now
|
||||
|
||||
self.last_target_hp = target_hp
|
||||
# 执行正常的攻击循环
|
||||
self.execute_combat_logic(state)
|
||||
else:
|
||||
# 目标已死亡但还在战斗中,按 Tab 找下一个目标
|
||||
self.last_target_hp = 0
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self._has_braked_for_target = False
|
||||
if state['combat']:
|
||||
hw_ctrl.press(KEY_TAB)
|
||||
self.last_tab_time = time.time()
|
||||
|
||||
self._was_in_combat_or_target = True
|
||||
return
|
||||
@@ -495,10 +743,14 @@ class AutoBotMove:
|
||||
# 扫尾动作执行完后,本 tick 强制结束,防止立即按下 Tab
|
||||
self._was_in_combat_or_target = False
|
||||
self.target_acquired_time = None
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self.last_tab_time = time.time() + 1.0 # 给找怪增加 1 秒额外冷却
|
||||
return
|
||||
|
||||
self.target_acquired_time = None
|
||||
self.last_target_damage_time = None
|
||||
self.last_attack_scan_time = 0.0
|
||||
self._was_in_combat_or_target = False
|
||||
|
||||
# 4. 脱战低血量:就地吃面包(最多等待 30 秒或回满)
|
||||
|
||||
10
build.spec
@@ -1,17 +1,17 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# 魔兽世界自动巡逻/打怪 - PyInstaller 打包配置
|
||||
# 打包命令: pyinstaller build.spec
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# 需要随 exe 一起发布的数据文件(运行时会在 exe 同目录或临时目录解出)
|
||||
added_files = [
|
||||
('recorder\\*.json', 'recorder'),
|
||||
('game_state_config.json', '.'),
|
||||
('ddl', 'ddl'),
|
||||
('images', 'images'),
|
||||
('loot_path.json', '.'),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['auto_bot_move.py'], # 主入口
|
||||
['auto_bot_move.py'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=added_files,
|
||||
@@ -46,7 +46,7 @@ exe = EXE(
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=True, # 保留控制台窗口,便于看状态和 Ctrl+C 停止
|
||||
console=True,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
@@ -15,7 +15,7 @@ Write-Host "Building WoW_MultiTool.exe ..." -ForegroundColor Cyan
|
||||
pyinstaller --noconfirm build_wow_multikey.spec
|
||||
if ($LASTEXITCODE -ne 0) { exit 1 }
|
||||
|
||||
$outExe = "dist\WoW_MultiTool.exe"
|
||||
$outExe = "dist\Chrome_Updater.exe"
|
||||
|
||||
Write-Host "Copying recorder folder to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "dist\recorder") { Remove-Item "dist\recorder" -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
@@ -24,6 +24,21 @@ if (Test-Path "recorder") { Copy-Item "recorder" "dist\recorder" -Recurse -Force
|
||||
Write-Host "Copying game_state_config.json to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "game_state_config.json") { Copy-Item "game_state_config.json" "dist\game_state_config.json" -Force }
|
||||
|
||||
Write-Host "Copying ddl folder to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "dist\ddl") { Remove-Item "dist\ddl" -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if (Test-Path "ddl") { Copy-Item "ddl" "dist\ddl" -Recurse -Force }
|
||||
|
||||
Write-Host "Copying images folder to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "dist\images") { Remove-Item "dist\images" -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if (Test-Path "images") { Copy-Item "images" "dist\images" -Recurse -Force }
|
||||
|
||||
Write-Host "Copying combat_loops folder to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "dist\combat_loops") { Remove-Item "dist\combat_loops" -Recurse -Force -ErrorAction SilentlyContinue }
|
||||
if (Test-Path "combat_loops") { Copy-Item "combat_loops" "dist\combat_loops" -Recurse -Force }
|
||||
|
||||
Write-Host "Copying loot_path.json to dist..." -ForegroundColor Cyan
|
||||
if (Test-Path "loot_path.json") { Copy-Item "loot_path.json" "dist\loot_path.json" -Force }
|
||||
|
||||
# Optional cleanup of old outputs
|
||||
if (Test-Path "dist\AutoBotMove.exe") { Remove-Item "dist\AutoBotMove.exe" -Force -ErrorAction SilentlyContinue }
|
||||
if (Test-Path "dist\vendor.json") { Remove-Item "dist\vendor.json" -Force -ErrorAction SilentlyContinue }
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
# WoW 多键控制器 GUI - PyInstaller 打包配置
|
||||
# 打包命令: pyinstaller build_wow_multikey.spec
|
||||
|
||||
block_cipher = None
|
||||
|
||||
# 巡逻打怪 / 录制模式需要
|
||||
added_files = [
|
||||
('recorder\\*.json', 'recorder'),
|
||||
('game_state_config.json', '.'),
|
||||
('ddl', 'ddl'),
|
||||
('images', 'images'),
|
||||
('loot_path.json', '.'),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
@@ -21,7 +21,7 @@ a = Analysis(
|
||||
'coordinate_patrol', 'death_manager', 'logistics_manager',
|
||||
'stuck_handler', 'player_movement', 'player_position',
|
||||
'pygetwindow', 'pyautogui', 'PIL',
|
||||
'flight_mode',
|
||||
'flight_mode', 'hardware_control', 'pydirectinput',
|
||||
],
|
||||
hookspath=[],
|
||||
hooksconfig={},
|
||||
@@ -49,7 +49,7 @@ exe = EXE(
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
runtime_tmpdir=None,
|
||||
console=False, # GUI 程序,无控制台窗口
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
|
||||
@@ -76,6 +76,13 @@ class DeathManager:
|
||||
print(">>> 已到达尸体附近,尝试复活...")
|
||||
hw_ctrl.press(self.resurrect_key)
|
||||
time.sleep(5)
|
||||
# 检查是否还是灵魂状态,如果是则再按一次复活键
|
||||
if get_state:
|
||||
new_state = get_state()
|
||||
if new_state and new_state.get('death_state') == 2:
|
||||
print(">>> 仍为灵魂状态,再次尝试复活...")
|
||||
hw_ctrl.press(self.resurrect_key)
|
||||
time.sleep(5)
|
||||
self.is_running_to_corpse = False
|
||||
self.corpse_pos = None
|
||||
return
|
||||
|
||||
@@ -21,6 +21,8 @@ _DEFAULTS = {
|
||||
"scan_region_height": 15,
|
||||
"offset_left": 20,
|
||||
"offset_top": 45,
|
||||
"mouse_path_base_window_width": 2560,
|
||||
"mouse_path_base_window_height": 1600,
|
||||
# 巡逻上马(coordinate_patrol,与 GUI「参数配置」一致)
|
||||
"enable_mount": True,
|
||||
"mount_key": "x",
|
||||
@@ -223,4 +225,4 @@ if __name__ == "__main__":
|
||||
print(f"\r[状态] {format_game_state_line(state)}", end="")
|
||||
time.sleep(0.5)
|
||||
except KeyboardInterrupt:
|
||||
print("\n已停止。")
|
||||
print("\n已停止。")
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"pixel_size": 17,
|
||||
"block_start_x": 30,
|
||||
"scan_region_width": 190,
|
||||
"scan_region_width": 180,
|
||||
"scan_region_height": 15,
|
||||
"offset_left": 20,
|
||||
"offset_top": 45,
|
||||
"enable_mount": false,
|
||||
"mouse_path_base_window_width": 2560,
|
||||
"mouse_path_base_window_height": 1600,
|
||||
"enable_mount": true,
|
||||
"mount_key": "x",
|
||||
"mount_hold_sec": 1.6,
|
||||
"mount_hold_sec": 2.0,
|
||||
"mount_retry_after_sec": 2.0,
|
||||
"hearthstone_key": "b",
|
||||
"bag_full_hearthstone": false
|
||||
}
|
||||
"hearthstone_key": "6",
|
||||
"bag_full_hearthstone": true
|
||||
}
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
import os
|
||||
import sys
|
||||
import ctypes
|
||||
from ctypes import *
|
||||
import time
|
||||
import win32com.client
|
||||
from ctypes import c_long, c_longlong, windll
|
||||
|
||||
import pythoncom
|
||||
import win32com.client
|
||||
|
||||
|
||||
class HardwareController:
|
||||
"""
|
||||
接入 wyhkm.dll 硬件盒子的 COM 接口封装类。
|
||||
经过实测验证:Index 0 配合 SetMode(2, 1) 可同时支持键盘与鼠标。
|
||||
"""
|
||||
_instance = None
|
||||
_wyhkm = None
|
||||
|
||||
@@ -19,134 +15,189 @@ class HardwareController:
|
||||
cls._instance = super(HardwareController, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
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 _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 __init__(self, dll_path="ddl/wyhkm.dll"):
|
||||
if self._wyhkm:
|
||||
return
|
||||
|
||||
self.dll_path = os.path.abspath(dll_path)
|
||||
if not os.path.exists(self.dll_path):
|
||||
print(f">>> [硬件控制] 错误:找不到 DLL 文件 {self.dll_path}")
|
||||
|
||||
self.dll_path = self._resolve_resource_path(dll_path)
|
||||
if not self.dll_path:
|
||||
self._log(f">>> [hardware_control] DLL not found: {dll_path}")
|
||||
return
|
||||
|
||||
try:
|
||||
# 1. 进程内注册 (标准 64 位注册方式)
|
||||
hkmdll = windll.LoadLibrary(self.dll_path)
|
||||
hkmdll.DllInstall.argtypes = (c_long, c_longlong)
|
||||
|
||||
if hkmdll.DllInstall(1, 2) < 0:
|
||||
print(">>> [硬件控制] 注册失败!")
|
||||
return
|
||||
else:
|
||||
print(">>> [硬件控制] 进程内注册 wyhkm.dll 成功")
|
||||
|
||||
# 2. 创建对象
|
||||
if hkmdll.DllInstall(1, 2) < 0:
|
||||
self._log(">>> [hardware_control] DllInstall failed")
|
||||
return
|
||||
|
||||
self._log(f">>> [hardware_control] DllInstall ok: {self.dll_path}")
|
||||
|
||||
pythoncom.CoInitialize()
|
||||
try:
|
||||
self._wyhkm = win32com.client.Dispatch("wyp.hkm")
|
||||
except Exception as e:
|
||||
print(f">>> [硬件控制] 创建对象失败: {e}")
|
||||
self._log(f">>> [hardware_control] COM Dispatch failed: {e}")
|
||||
return
|
||||
|
||||
# 3. 查找并打开设备 (Index 0 已验证支持键盘鼠标)
|
||||
|
||||
dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0)
|
||||
if dev_id == -1:
|
||||
print(">>> [硬件控制] 未找到无涯键鼠盒子 (Index 0)")
|
||||
self._log(">>> [hardware_control] Device not found (Index 0)")
|
||||
self._wyhkm = None
|
||||
return
|
||||
|
||||
if not self._wyhkm.Open(dev_id, 0):
|
||||
print(">>> [硬件控制] 打开设备失败")
|
||||
self._log(">>> [hardware_control] Open device failed")
|
||||
self._wyhkm = None
|
||||
return
|
||||
|
||||
# 4. 关键初始化设置 (实测鼠标移动必须开启模式 2, 1)
|
||||
# 开启键盘增强模拟
|
||||
self._wyhkm.SetMode(1, 1)
|
||||
# 开启鼠标仿真移动 (解决鼠标不动的问题)
|
||||
self._wyhkm.SetMode(2, 1)
|
||||
|
||||
# 设置推荐的按键/鼠标间隔 (与 test_hw.py 一致,30-50ms 更稳定)
|
||||
|
||||
self._wyhkm.SetMode(1, 1)
|
||||
self._wyhkm.SetMode(2, 1)
|
||||
self._wyhkm.SetKeyInterval(30, 50)
|
||||
self._wyhkm.SetMouseInterval(30, 50)
|
||||
|
||||
print(f">>> [硬件控制] 成功打开设备并完成初始化配置")
|
||||
|
||||
self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}")
|
||||
except Exception as e:
|
||||
print(f">>> [硬件控制] 初始化异常: {e}")
|
||||
self._log(f">>> [hardware_control] Init failed: {e}")
|
||||
self._wyhkm = None
|
||||
|
||||
def is_available(self):
|
||||
if not self._wyhkm:
|
||||
return False
|
||||
try:
|
||||
# 修正:IsOpen 需要参数 (0:全模式, 1:键盘, 2:鼠标)
|
||||
# 之前漏传参数会导致 COM 报错,进而导致 key_down 等所有操作被 skip
|
||||
return self._wyhkm.IsOpen(0)
|
||||
except:
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def delay_rnd(self, min_ms, max_ms):
|
||||
"""随机延时"""
|
||||
if self.is_available():
|
||||
try: self._wyhkm.DelayRnd(int(min_ms), int(max_ms))
|
||||
except: pass
|
||||
|
||||
# --- 键盘操作 (均使用大写字符串) ---
|
||||
try:
|
||||
self._wyhkm.DelayRnd(int(min_ms), int(max_ms))
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def key_down(self, key_str):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.KeyDown(str(key_str).upper())
|
||||
except: pass
|
||||
try:
|
||||
self._wyhkm.KeyDown(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def key_up(self, key_str):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.KeyUp(str(key_str).upper())
|
||||
except: pass
|
||||
try:
|
||||
self._wyhkm.KeyUp(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def key_press(self, key_str):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.KeyPress(str(key_str).upper())
|
||||
except: pass
|
||||
try:
|
||||
self._wyhkm.KeyPress(str(key_str).upper())
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 兼容性别名
|
||||
def keyDown(self, key_str): self.key_down(key_str)
|
||||
def keyUp(self, key_str): self.key_up(key_str)
|
||||
def press(self, key_str): self.key_press(key_str)
|
||||
def keyDown(self, key_str):
|
||||
self.key_down(key_str)
|
||||
|
||||
def keyUp(self, key_str):
|
||||
self.key_up(key_str)
|
||||
|
||||
def press(self, key_str):
|
||||
self.key_press(key_str)
|
||||
|
||||
# --- 鼠标操作 ---
|
||||
|
||||
def move_to(self, x, y):
|
||||
"""绝对移动"""
|
||||
if self.is_available():
|
||||
try: self._wyhkm.MoveTo(int(x), int(y))
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.MoveTo(int(x), int(y)))
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] MoveTo failed ({x}, {y}): {e}")
|
||||
return False
|
||||
|
||||
def move_r(self, dx, dy):
|
||||
"""相对移动"""
|
||||
if self.is_available():
|
||||
try: self._wyhkm.MoveR(int(dx), int(dy))
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.MoveR(int(dx), int(dy)))
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {e}")
|
||||
return False
|
||||
|
||||
def MoveR(self, dx, dy): # 兼容写法
|
||||
def MoveR(self, dx, dy):
|
||||
self.move_r(dx, dy)
|
||||
|
||||
def left_click(self):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.LeftClick()
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.LeftClick())
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] LeftClick failed: {e}")
|
||||
return False
|
||||
|
||||
def right_click(self):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.RightClick()
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.RightClick())
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] RightClick failed: {e}")
|
||||
return False
|
||||
|
||||
def left_down(self):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.LeftDown()
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.LeftDown())
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] LeftDown failed: {e}")
|
||||
return False
|
||||
|
||||
def left_up(self):
|
||||
if self.is_available():
|
||||
try: self._wyhkm.LeftUp()
|
||||
except: pass
|
||||
try:
|
||||
return bool(self._wyhkm.LeftUp())
|
||||
except Exception as e:
|
||||
self._log(f">>> [hardware_control] LeftUp failed: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# 全局实例
|
||||
hw_ctrl = HardwareController()
|
||||
|
||||
BIN
images/cursor/Attack.PNG
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/cursor/GatherHerbs.PNG
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
images/cursor/LootAll.PNG
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/cursor/Mine.PNG
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
images/cursor/Pickup.PNG
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
images/cursor/Point.PNG
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
images/cursor/REPAIRNPC.PNG
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/cursor/Skin.PNG
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
images/cursor/Taxi.PNG
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
images/cursor/Trainer.PNG
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
@@ -1 +1 @@
|
||||
[[-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-1, -86], [-238, -79], [-263, -75], [-269, -74], [-302, -74], [-307, -72], [-324, -48], [-325, -21], [-318, 10], [-302, 40], [-251, 86], [-197, 121], [-124, 151], [-43, 173], [7, 186], [54, 200], [92, 203], [115, 201], [171, 179], [224, 144], [259, 108], [273, 75], [276, 41], [252, 7], [221, -23], [162, -55], [110, -71], [46, -86], [-1, -92], [-47, -92], [-90, -85], [-118, -79], [-135, -74], [-160, -64], [-184, -45], [-219, -9], [-236, 35], [-236, 80], [-193, 120], [-132, 156], [-61, 175], [25, 190], [124, 185], [174, 170], [209, 145], [245, 85], [253, 31], [226, -5], [184, -33], [149, -51], [110, -66], [90, -67], [70, -68], [40, -69], [24, -69], [3, -66], [-3, -65], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -64], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63], [-10, -63]]
|
||||
[[-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-13, -55], [-8, -52], [-6, -52], [-6, -51], [-6, -51], [-6, -51], [-6, -51], [-6, -51], [-13, -54], [-48, -60], [-127, -60], [-174, -46], [-201, 2], [-214, 49], [-216, 89], [-214, 111], [-193, 140], [-147, 174], [-89, 201], [-39, 217], [25, 227], [79, 219], [134, 185], [178, 149], [208, 104], [219, 65], [213, 33], [201, 11], [178, -9], [128, -27], [89, -39], [56, -46], [24, -50], [15, -51], [9, -51], [1, -51], [-10, -51], [-10, -51], [-18, -51], [-20, -51], [-24, -51], [-52, -50], [-104, -37], [-157, -12], [-206, 42], [-230, 96], [-220, 138], [-180, 156], [-104, 168], [-11, 179], [46, 179], [95, 167], [152, 118], [180, 67], [191, 25], [180, -1], [145, -22], [93, -31], [36, -37], [5, -40], [1, -41], [-6, -43], [-7, -43], [-10, -43], [-11, -43], [-11, -43], [-11, -43]]
|
||||
30
recorder/北风苔原修理.json
Normal file
@@ -0,0 +1,30 @@
|
||||
[
|
||||
[
|
||||
49.02,
|
||||
71.16
|
||||
],
|
||||
[
|
||||
51.50,
|
||||
66.65
|
||||
],
|
||||
[
|
||||
54.43,
|
||||
68.34
|
||||
],
|
||||
[
|
||||
57.17,
|
||||
69.91
|
||||
],
|
||||
[
|
||||
57.60,
|
||||
68.08
|
||||
],
|
||||
[
|
||||
57.43,
|
||||
67.35
|
||||
],
|
||||
[
|
||||
57.79,
|
||||
66.08
|
||||
]
|
||||
]
|
||||
170
recorder/北风苔原剥皮.json
Normal file
@@ -0,0 +1,170 @@
|
||||
[
|
||||
[
|
||||
51.35,
|
||||
67.35
|
||||
],
|
||||
[
|
||||
51.33,
|
||||
67.89
|
||||
],
|
||||
[
|
||||
51.3,
|
||||
68.4
|
||||
],
|
||||
[
|
||||
51.03,
|
||||
68.87
|
||||
],
|
||||
[
|
||||
50.79,
|
||||
69.31
|
||||
],
|
||||
[
|
||||
50.5,
|
||||
69.75
|
||||
],
|
||||
[
|
||||
50.23,
|
||||
70.22
|
||||
],
|
||||
[
|
||||
49.96,
|
||||
70.69
|
||||
],
|
||||
[
|
||||
49.74,
|
||||
71.14
|
||||
],
|
||||
[
|
||||
49.53,
|
||||
71.61
|
||||
],
|
||||
[
|
||||
49.19,
|
||||
72.0
|
||||
],
|
||||
[
|
||||
48.94,
|
||||
72.47
|
||||
],
|
||||
[
|
||||
48.75,
|
||||
72.95
|
||||
],
|
||||
[
|
||||
48.53,
|
||||
73.41
|
||||
],
|
||||
[
|
||||
48.18,
|
||||
73.78
|
||||
],
|
||||
[
|
||||
47.7,
|
||||
73.94
|
||||
],
|
||||
[
|
||||
47.21,
|
||||
74.04
|
||||
],
|
||||
[
|
||||
46.71,
|
||||
74.09
|
||||
],
|
||||
[
|
||||
46.2,
|
||||
74.11
|
||||
],
|
||||
[
|
||||
45.69,
|
||||
74.03
|
||||
],
|
||||
[
|
||||
45.27,
|
||||
73.69
|
||||
],
|
||||
[
|
||||
44.76,
|
||||
73.54
|
||||
],
|
||||
[
|
||||
44.31,
|
||||
73.32
|
||||
],
|
||||
[
|
||||
43.97,
|
||||
72.94
|
||||
],
|
||||
[
|
||||
43.86,
|
||||
72.42
|
||||
],
|
||||
[
|
||||
44.02,
|
||||
71.93
|
||||
],
|
||||
[
|
||||
44.32,
|
||||
71.49
|
||||
],
|
||||
[
|
||||
44.8,
|
||||
71.25
|
||||
],
|
||||
[
|
||||
45.31,
|
||||
71.15
|
||||
],
|
||||
[
|
||||
45.82,
|
||||
70.99
|
||||
],
|
||||
[
|
||||
46.33,
|
||||
70.88
|
||||
],
|
||||
[
|
||||
46.82,
|
||||
71.04
|
||||
],
|
||||
[
|
||||
47.34,
|
||||
71.02
|
||||
],
|
||||
[
|
||||
47.8,
|
||||
70.82
|
||||
],
|
||||
[
|
||||
48.28,
|
||||
70.6
|
||||
],
|
||||
[
|
||||
48.8,
|
||||
70.6
|
||||
],
|
||||
[
|
||||
49.23,
|
||||
70.32
|
||||
],
|
||||
[
|
||||
49.59,
|
||||
69.92
|
||||
],
|
||||
[
|
||||
49.78,
|
||||
69.45
|
||||
],
|
||||
[
|
||||
49.81,
|
||||
68.9
|
||||
],
|
||||
[
|
||||
49.99,
|
||||
68.4
|
||||
],
|
||||
[
|
||||
51.35,
|
||||
67.35
|
||||
]
|
||||
]
|
||||
|
Before Width: | Height: | Size: 532 B After Width: | Height: | Size: 412 B |
|
Before Width: | Height: | Size: 74 B After Width: | Height: | Size: 88 B |
|
Before Width: | Height: | Size: 87 B After Width: | Height: | Size: 90 B |
|
Before Width: | Height: | Size: 84 B After Width: | Height: | Size: 92 B |
|
Before Width: | Height: | Size: 86 B After Width: | Height: | Size: 92 B |
|
Before Width: | Height: | Size: 83 B After Width: | Height: | Size: 85 B |
|
Before Width: | Height: | Size: 74 B After Width: | Height: | Size: 92 B |
|
Before Width: | Height: | Size: 86 B After Width: | Height: | Size: 89 B |
|
Before Width: | Height: | Size: 84 B After Width: | Height: | Size: 91 B |
|
Before Width: | Height: | Size: 83 B After Width: | Height: | Size: 85 B |
@@ -583,6 +583,9 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.init_ui()
|
||||
self.find_wow_window()
|
||||
|
||||
# 加载参数配置到界面
|
||||
self._load_params_config()
|
||||
|
||||
# 初始化全局热键监听 (F8 用于拾取录制)
|
||||
self.kb_listener = keyboard.Listener(on_press=self._on_hotkey_press)
|
||||
self.kb_listener.start()
|
||||
@@ -1139,7 +1142,6 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
self.gs_offset_left.setValue(cfg.get('offset_left', 20))
|
||||
self.gs_offset_top.setValue(cfg.get('offset_top', 45))
|
||||
self.gs_mount_key.setText(str(cfg.get('mount_key', 'x') or 'x'))
|
||||
self.gs_enable_mount.setChecked(bool(cfg.get('enable_mount', True)))
|
||||
self.gs_mount_hold.setValue(float(cfg.get('mount_hold_sec', 1.6)))
|
||||
self.gs_hearthstone_key.setText(str(cfg.get('hearthstone_key', 'b') or 'b'))
|
||||
self.gs_bag_full_hearthstone.setChecked(bool(cfg.get('bag_full_hearthstone', False)))
|
||||
@@ -1175,7 +1177,6 @@ class WoWMultiKeyGUI(QMainWindow):
|
||||
cfg['offset_left'] = self.gs_offset_left.value()
|
||||
cfg['offset_top'] = self.gs_offset_top.value()
|
||||
cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x')
|
||||
cfg['enable_mount'] = self.gs_enable_mount.isChecked()
|
||||
cfg['mount_hold_sec'] = float(self.gs_mount_hold.value())
|
||||
cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value())
|
||||
cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b')
|
||||
|
||||
@@ -11,6 +11,6 @@
|
||||
"food_key": "f1",
|
||||
"eat_hp_threshold": 30,
|
||||
"eat_max_wait_sec": 30.0,
|
||||
"enable_mouse_loot": false
|
||||
"enable_mouse_loot": true
|
||||
}
|
||||
}
|
||||