更新:添加鼠标图标识别、复活逻辑优化、参数配置加载修复、目标血量100%检测

This commit is contained in:
王鹏
2026-04-19 15:53:45 +08:00
parent 33dc741fd9
commit 6ec468a78b
46 changed files with 900 additions and 131 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -8,7 +8,6 @@ import ctypes
import cv2 import cv2
import numpy as np import numpy as np
import win32gui import win32gui
import win32api
import win32con import win32con
# 开启 DPI 意识 # 开启 DPI 意识
@@ -18,7 +17,7 @@ except Exception:
ctypes.windll.user32.SetProcessDPIAware() ctypes.windll.user32.SetProcessDPIAware()
from hardware_control import hw_ctrl 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 from stuck_handler import StuckHandler
# 定义按键常量 # 定义按键常量
@@ -32,16 +31,44 @@ def _config_base():
return os.path.dirname(sys.executable) return os.path.dirname(sys.executable)
return os.path.dirname(os.path.abspath(__file__)) 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: class CursorManager:
"""通过图像识别判断鼠标图标类型""" """通过图像识别判断鼠标图标类型"""
def __init__(self): def __init__(self):
self.templates = {} self.templates = {}
self.handle_cache = {} self.handle_cache = {}
self.log_path = get_config_path('cursor_recognition.log')
self._load_templates() self._load_templates()
def _load_templates(self): def _load_templates(self):
# 强制使用纯相对路径,由 Python 自动处理 CWD完美避开中文路径编码问题 # 强制使用纯相对路径,由 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'} files = {'Point': 'Point.PNG', 'Attack': 'Attack.PNG', 'LootAll': 'LootAll.PNG', 'Skin': 'Skin.PNG'}
for name, fname in files.items(): for name, fname in files.items():
path = os.path.join(cursor_dir, fname) path = os.path.join(cursor_dir, fname)
@@ -66,6 +93,33 @@ class CursorManager:
self.handle_cache[hcursor] = res self.handle_cache[hcursor] = res
return 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): def _identify(self, hcursor):
import win32ui import win32ui
try: try:
@@ -85,20 +139,37 @@ class CursorManager:
best_name = 'Other' best_name = 'Other'
max_score = 0.0 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(): for name, temp in self.templates.items():
t_h, t_w = temp.shape[:2] t_h, t_w = temp.shape[:2]
for s in scales: for s in scales:
s_w, s_h = int(t_w * s), int(t_h * s) s_w, s_h = int(t_w * s), int(t_h * s)
if s_w > width or s_h > height: continue if s_w > width or s_h > height:
res_temp = cv2.resize(temp, (s_w, s_h)) continue
res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED) interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC
_, score, _, _ = cv2.minMaxLoc(res) 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: if score > max_score:
max_score = score max_score = score
best_name = name best_name = name
best_scale = s
if max_score > 0.2: 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}") print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}")
return best_name, max_score return best_name, max_score
except Exception: 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.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.enable_mouse_loot = enable_mouse_loot
self.cursor_mgr = CursorManager() 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): def execute_disengage_loot(self):
"""从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。""" """从有战斗/目标切换到完全脱战的瞬间,执行拾取 + 剥皮。"""
@@ -159,8 +235,160 @@ class AutoBot:
except Exception: except Exception:
pass 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): 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 if random.random() < 0.1: return False
hwnd = win32gui.FindWindow(None, WIN_TITLE) hwnd = win32gui.FindWindow(None, WIN_TITLE)
if not hwnd: return False if not hwnd: return False
@@ -172,13 +400,12 @@ class AutoBot:
center_y = top + (bottom - top) // 2 center_y = top + (bottom - top) // 2
# 1. 强制“角落校准”采样 # 1. 强制“角落校准”采样
win32api.SetCursorPos((left + 50, top + 50)) move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
time.sleep(0.2)
_, default_hcursor, _ = win32gui.GetCursorInfo() _, default_hcursor, _ = win32gui.GetCursorInfo()
# 2. 获取扫瞄路径点位 # 2. 获取扫瞄路径点位
path_points = [] path_points = []
path_file = "loot_path.json" path_file = get_config_path("loot_path.json")
if os.path.exists(path_file): if os.path.exists(path_file):
with open(path_file, 'r') as f: with open(path_file, 'r') as f:
path_points = json.load(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 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 if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue
win32api.SetCursorPos((target_x, target_y)) move_cursor_hw(target_x, target_y, settle_sec=0.02)
time.sleep(0.02)
_, hcursor, _ = win32gui.GetCursorInfo() _, hcursor, _ = win32gui.GetCursorInfo()
if hcursor != 0 and hcursor != default_hcursor: if hcursor != 0 and hcursor != default_hcursor:
@@ -231,7 +457,7 @@ class AutoBot:
elif score > 0.4: elif score > 0.4:
print(f">>> [扫雷] 疑似图标: {ctype_name} (得分: {score:.3f} < 门槛 0.7)") 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 return True
except Exception as e: except Exception as e:
print(f">>> [扫雷拾取] 出错: {e}") print(f">>> [扫雷拾取] 出错: {e}")
@@ -288,10 +514,19 @@ class AutoBot:
self._has_braked_for_target = True self._has_braked_for_target = True
# 2. 交互逻辑 # 2. 交互逻辑
cooldown = 2.0 if not state['combat'] else 6.0 hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp
if is_new_target or (current_time - self.last_interaction_time > cooldown): if is_new_target or hp_dropped or self.last_target_damage_time is None:
hw_ctrl.press(KEY_LOOT) self.last_target_damage_time = current_time
self.last_interaction_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 self.last_target_hp = target_hp
if state['combat']: if state['combat']:
self.execute_combat_logic(state) self.execute_combat_logic(state)
@@ -304,11 +539,15 @@ class AutoBot:
self._was_in_combat_or_target = False self._was_in_combat_or_target = False
self.last_tab_time = current_time + 1.0 self.last_tab_time = current_time + 1.0
self.last_target_hp = 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 self._has_braked_for_target = False
return return
self._was_in_combat_or_target = False self._was_in_combat_or_target = False
self.last_target_hp = 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 self._has_braked_for_target = False
self.tab_no_target_count = min(self.tab_no_target_count, 5) self.tab_no_target_count = min(self.tab_no_target_count, 5)

View File

@@ -7,6 +7,8 @@ import math
import ctypes import ctypes
import cv2 import cv2
import numpy as np import numpy as np
import win32con
import win32gui
from hardware_control import hw_ctrl from hardware_control import hw_ctrl
from game_state import parse_game_state, load_layout_config from game_state import parse_game_state, load_layout_config
from coordinate_patrol import CoordinatePatrol from coordinate_patrol import CoordinatePatrol
@@ -28,6 +30,11 @@ WIN_TITLE = "魔兽世界"
# 默认巡逻航点waypoints.json 不存在或无效时使用) # 默认巡逻航点waypoints.json 不存在或无效时使用)
DEFAULT_WAYPOINTS = [(23.8, 71.0), (27.0, 79.0), (31.0, 72.0)] 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(): def _config_base():
"""打包成 exe 时,优先从 exe 同目录读配置,否则用内嵌资源目录。""" """打包成 exe 时,优先从 exe 同目录读配置,否则用内嵌资源目录。"""
@@ -54,6 +61,19 @@ def get_config_path(filename):
return os.path.join(base, 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): def load_waypoints(path=None):
path = path or get_config_path(WAYPOINTS_FILE) path = path or get_config_path(WAYPOINTS_FILE)
if os.path.exists(path): if os.path.exists(path):
@@ -90,11 +110,12 @@ class CursorManager:
def __init__(self): def __init__(self):
self.templates = {} self.templates = {}
self.handle_cache = {} # 句柄 -> 类型缓存 self.handle_cache = {} # 句柄 -> 类型缓存
self.log_path = get_config_path('cursor_recognition.log')
self._load_templates() self._load_templates()
def _load_templates(self): def _load_templates(self):
# 强制使用纯相对路径,不带盘符前缀,由 Python 自动处理 CWD # 强制使用纯相对路径,不带盘符前缀,由 Python 自动处理 CWD
cursor_dir = os.path.join('images', 'cursor') cursor_dir = get_config_path(os.path.join('images', 'cursor'))
files = { files = {
'Point': 'Point.PNG', 'Point': 'Point.PNG',
'Attack': 'Attack.PNG', 'Attack': 'Attack.PNG',
@@ -125,6 +146,33 @@ class CursorManager:
self.handle_cache[hcursor] = res self.handle_cache[hcursor] = res
return 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): def _identify(self, hcursor):
import win32ui import win32ui
try: try:
@@ -146,21 +194,40 @@ class CursorManager:
max_score = 0.0 max_score = 0.0
# 多尺度匹配:尝试 0.8, 1.0, 1.2 倍缩放,解决 UI 缩放问题 # 多尺度匹配:尝试 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(): for name, temp in self.templates.items():
t_h, t_w = temp.shape[:2] t_h, t_w = temp.shape[:2]
for s in scales: for s in scales:
s_w, s_h = int(t_w * s), int(t_h * s) s_w, s_h = int(t_w * s), int(t_h * s)
if s_w > width or s_h > height: continue if s_w > width or s_h > height:
continue
res_temp = cv2.resize(temp, (s_w, s_h)) interp = cv2.INTER_AREA if s < 1.0 else cv2.INTER_CUBIC
res = cv2.matchTemplate(target, res_temp, cv2.TM_CCOEFF_NORMED) res_temp = cv2.resize(temp, (s_w, s_h), interpolation=interp)
_, score, _, _ = cv2.minMaxLoc(res) 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: if score > max_score:
max_score = score max_score = score
best_name = name best_name = name
best_scale = s
if max_score > 0.2: 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}") print(f">>>> [雷达识别] 目标: {best_name} | 最高分: {max_score:.3f}")
return best_name, max_score return best_name, max_score
except Exception as e: except Exception as e:
@@ -202,6 +269,11 @@ class AutoBotMove:
self.attack_loop_config = load_attack_loop(attack_loop_path) self.attack_loop_config = load_attack_loop(attack_loop_path)
self._prev_death_state = 0 self._prev_death_state = 0
self._eating_started_at = None 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 表示需要立即停止(用于中断阻塞中的后勤/路线导航) # stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航)
self._stop_check = stop_check if callable(stop_check) else (lambda: False) self._stop_check = stop_check if callable(stop_check) else (lambda: False)
if waypoints is None: if waypoints is None:
@@ -259,16 +331,173 @@ class AutoBotMove:
else: else:
# 关闭扫雷时,执行基础的交互拾取 # 关闭扫雷时,执行基础的交互拾取
hw_ctrl.press(KEY_LOOT) hw_ctrl.press(KEY_LOOT)
time.sleep(0.5) time.sleep(0.8)
# 2. 最后补漏剥皮(针对脚下尸体) # 2. 最后补漏剥皮(针对脚下尸体)
# hw_ctrl.press(KEY_LOOT) hw_ctrl.press(KEY_LOOT)
# time.sleep(self.skinning_wait_sec + 0.5) time.sleep(self.skinning_wait_sec + 0.5)
except Exception: except Exception:
pass 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): 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 if random.random() < 0.1: return False
hwnd = win32gui.FindWindow(None, WIN_TITLE) hwnd = win32gui.FindWindow(None, WIN_TITLE)
if not hwnd: return False if not hwnd: return False
@@ -280,8 +509,7 @@ class AutoBotMove:
center_y = top + (bottom - top) // 2 center_y = top + (bottom - top) // 2
# 1. 强制“角落校准”采样 # 1. 强制“角落校准”采样
win32api.SetCursorPos((left + 50, top + 50)) move_cursor_hw(left + 50, top + 50, settle_sec=0.2)
time.sleep(0.2)
_, default_hcursor, _ = win32gui.GetCursorInfo() _, default_hcursor, _ = win32gui.GetCursorInfo()
# 2. 获取扫瞄路径点位 # 2. 获取扫瞄路径点位
@@ -312,8 +540,7 @@ class AutoBotMove:
if not (left+10 < target_x < right-10 and top+10 < target_y < bottom-10): continue 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 if any(math.dist((target_x, target_y), pos) < 30 for pos in looted_positions): continue
win32api.SetCursorPos((target_x, target_y)) move_cursor_hw(target_x, target_y, settle_sec=0.02)
time.sleep(0.02)
_, hcursor, _ = win32gui.GetCursorInfo() _, hcursor, _ = win32gui.GetCursorInfo()
if hcursor != 0 and hcursor != default_hcursor: if hcursor != 0 and hcursor != default_hcursor:
@@ -343,7 +570,7 @@ class AutoBotMove:
if self._should_stop(): break if self._should_stop(): break
win32api.SetCursorPos((center_x, center_y)) move_cursor_hw(center_x, center_y, settle_sec=0.02)
return True return True
except Exception as e: except Exception as e:
print(f">>> [扫雷拾取] 出错: {e}") print(f">>> [扫雷拾取] 出错: {e}")
@@ -472,17 +699,38 @@ class AutoBotMove:
self._has_braked_for_target = True self._has_braked_for_target = True
# 2. 交互键KEY_LOOT按键策略 # 2. 交互键KEY_LOOT按键策略
cooldown = 2.0 if not state['combat'] else 6.0 hp_dropped = self.last_target_hp > 0 and target_hp < self.last_target_hp
if is_new_target or (now - self.last_interaction_time > cooldown): 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) hw_ctrl.press(KEY_LOOT)
self.last_interaction_time = now 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.last_target_hp = target_hp
# 执行正常的攻击循环 # 执行正常的攻击循环
self.execute_combat_logic(state) self.execute_combat_logic(state)
else: else:
# 目标已死亡但还在战斗中,按 Tab 找下一个目标
self.last_target_hp = 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 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 self._was_in_combat_or_target = True
return return
@@ -495,10 +743,14 @@ class AutoBotMove:
# 扫尾动作执行完后,本 tick 强制结束,防止立即按下 Tab # 扫尾动作执行完后,本 tick 强制结束,防止立即按下 Tab
self._was_in_combat_or_target = False self._was_in_combat_or_target = False
self.target_acquired_time = None 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 秒额外冷却 self.last_tab_time = time.time() + 1.0 # 给找怪增加 1 秒额外冷却
return return
self.target_acquired_time = None 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 self._was_in_combat_or_target = False
# 4. 脱战低血量:就地吃面包(最多等待 30 秒或回满) # 4. 脱战低血量:就地吃面包(最多等待 30 秒或回满)

View File

@@ -1,17 +1,17 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
# 魔兽世界自动巡逻/打怪 - PyInstaller 打包配置
# 打包命令: pyinstaller build.spec
block_cipher = None block_cipher = None
# 需要随 exe 一起发布的数据文件(运行时会在 exe 同目录或临时目录解出)
added_files = [ added_files = [
('recorder\\*.json', 'recorder'), ('recorder\\*.json', 'recorder'),
('game_state_config.json', '.'), ('game_state_config.json', '.'),
('ddl', 'ddl'),
('images', 'images'),
('loot_path.json', '.'),
] ]
a = Analysis( a = Analysis(
['auto_bot_move.py'], # 主入口 ['auto_bot_move.py'],
pathex=[], pathex=[],
binaries=[], binaries=[],
datas=added_files, datas=added_files,
@@ -46,7 +46,7 @@ exe = EXE(
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=True, # 保留控制台窗口,便于看状态和 Ctrl+C 停止 console=True,
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,

View File

@@ -15,7 +15,7 @@ Write-Host "Building WoW_MultiTool.exe ..." -ForegroundColor Cyan
pyinstaller --noconfirm build_wow_multikey.spec pyinstaller --noconfirm build_wow_multikey.spec
if ($LASTEXITCODE -ne 0) { exit 1 } 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 Write-Host "Copying recorder folder to dist..." -ForegroundColor Cyan
if (Test-Path "dist\recorder") { Remove-Item "dist\recorder" -Recurse -Force -ErrorAction SilentlyContinue } 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 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 } 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 # Optional cleanup of old outputs
if (Test-Path "dist\AutoBotMove.exe") { Remove-Item "dist\AutoBotMove.exe" -Force -ErrorAction SilentlyContinue } 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 } if (Test-Path "dist\vendor.json") { Remove-Item "dist\vendor.json" -Force -ErrorAction SilentlyContinue }

View File

@@ -1,13 +1,13 @@
# -*- mode: python ; coding: utf-8 -*- # -*- mode: python ; coding: utf-8 -*-
# WoW 多键控制器 GUI - PyInstaller 打包配置
# 打包命令: pyinstaller build_wow_multikey.spec
block_cipher = None block_cipher = None
# 巡逻打怪 / 录制模式需要
added_files = [ added_files = [
('recorder\\*.json', 'recorder'), ('recorder\\*.json', 'recorder'),
('game_state_config.json', '.'), ('game_state_config.json', '.'),
('ddl', 'ddl'),
('images', 'images'),
('loot_path.json', '.'),
] ]
a = Analysis( a = Analysis(
@@ -21,7 +21,7 @@ a = Analysis(
'coordinate_patrol', 'death_manager', 'logistics_manager', 'coordinate_patrol', 'death_manager', 'logistics_manager',
'stuck_handler', 'player_movement', 'player_position', 'stuck_handler', 'player_movement', 'player_position',
'pygetwindow', 'pyautogui', 'PIL', 'pygetwindow', 'pyautogui', 'PIL',
'flight_mode', 'flight_mode', 'hardware_control', 'pydirectinput',
], ],
hookspath=[], hookspath=[],
hooksconfig={}, hooksconfig={},
@@ -49,7 +49,7 @@ exe = EXE(
upx=True, upx=True,
upx_exclude=[], upx_exclude=[],
runtime_tmpdir=None, runtime_tmpdir=None,
console=False, # GUI 程序,无控制台窗口 console=False,
disable_windowed_traceback=False, disable_windowed_traceback=False,
argv_emulation=False, argv_emulation=False,
target_arch=None, target_arch=None,

View File

@@ -76,6 +76,13 @@ class DeathManager:
print(">>> 已到达尸体附近,尝试复活...") print(">>> 已到达尸体附近,尝试复活...")
hw_ctrl.press(self.resurrect_key) hw_ctrl.press(self.resurrect_key)
time.sleep(5) 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.is_running_to_corpse = False
self.corpse_pos = None self.corpse_pos = None
return return

View File

@@ -21,6 +21,8 @@ _DEFAULTS = {
"scan_region_height": 15, "scan_region_height": 15,
"offset_left": 20, "offset_left": 20,
"offset_top": 45, "offset_top": 45,
"mouse_path_base_window_width": 2560,
"mouse_path_base_window_height": 1600,
# 巡逻上马coordinate_patrol与 GUI「参数配置」一致 # 巡逻上马coordinate_patrol与 GUI「参数配置」一致
"enable_mount": True, "enable_mount": True,
"mount_key": "x", "mount_key": "x",

View File

@@ -1,14 +1,16 @@
{ {
"pixel_size": 17, "pixel_size": 17,
"block_start_x": 30, "block_start_x": 30,
"scan_region_width": 190, "scan_region_width": 180,
"scan_region_height": 15, "scan_region_height": 15,
"offset_left": 20, "offset_left": 20,
"offset_top": 45, "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_key": "x",
"mount_hold_sec": 1.6, "mount_hold_sec": 2.0,
"mount_retry_after_sec": 2.0, "mount_retry_after_sec": 2.0,
"hearthstone_key": "b", "hearthstone_key": "6",
"bag_full_hearthstone": false "bag_full_hearthstone": true
} }

View File

@@ -1,16 +1,12 @@
import os import os
import sys import sys
import ctypes from ctypes import c_long, c_longlong, windll
from ctypes import *
import time
import win32com.client
import pythoncom import pythoncom
import win32com.client
class HardwareController: class HardwareController:
"""
接入 wyhkm.dll 硬件盒子的 COM 接口封装类。
经过实测验证Index 0 配合 SetMode(2, 1) 可同时支持键盘与鼠标。
"""
_instance = None _instance = None
_wyhkm = None _wyhkm = None
@@ -19,134 +15,189 @@ class HardwareController:
cls._instance = super(HardwareController, cls).__new__(cls) cls._instance = super(HardwareController, cls).__new__(cls)
return cls._instance 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"): def __init__(self, dll_path="ddl/wyhkm.dll"):
if self._wyhkm: if self._wyhkm:
return return
self.dll_path = os.path.abspath(dll_path) self.dll_path = self._resolve_resource_path(dll_path)
if not os.path.exists(self.dll_path): if not self.dll_path:
print(f">>> [硬件控制] 错误:找不到 DLL 文件 {self.dll_path}") self._log(f">>> [hardware_control] DLL not found: {dll_path}")
return return
try: try:
# 1. 进程内注册 (标准 64 位注册方式)
hkmdll = windll.LoadLibrary(self.dll_path) hkmdll = windll.LoadLibrary(self.dll_path)
hkmdll.DllInstall.argtypes = (c_long, c_longlong) hkmdll.DllInstall.argtypes = (c_long, c_longlong)
if hkmdll.DllInstall(1, 2) < 0: if hkmdll.DllInstall(1, 2) < 0:
print(">>> [硬件控制] 注册失败!") self._log(">>> [hardware_control] DllInstall failed")
return return
else:
print(">>> [硬件控制] 进程内注册 wyhkm.dll 成功")
# 2. 创建对象 self._log(f">>> [hardware_control] DllInstall ok: {self.dll_path}")
pythoncom.CoInitialize() pythoncom.CoInitialize()
try: try:
self._wyhkm = win32com.client.Dispatch("wyp.hkm") self._wyhkm = win32com.client.Dispatch("wyp.hkm")
except Exception as e: except Exception as e:
print(f">>> [硬件控制] 创建对象失败: {e}") self._log(f">>> [hardware_control] COM Dispatch failed: {e}")
return return
# 3. 查找并打开设备 (Index 0 已验证支持键盘鼠标)
dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0) dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0)
if dev_id == -1: if dev_id == -1:
print(">>> [硬件控制] 未找到无涯键鼠盒子 (Index 0)") self._log(">>> [hardware_control] Device not found (Index 0)")
self._wyhkm = None
return return
if not self._wyhkm.Open(dev_id, 0): if not self._wyhkm.Open(dev_id, 0):
print(">>> [硬件控制] 打开设备失败") self._log(">>> [hardware_control] Open device failed")
self._wyhkm = None
return return
# 4. 关键初始化设置 (实测鼠标移动必须开启模式 2, 1)
# 开启键盘增强模拟
self._wyhkm.SetMode(1, 1) self._wyhkm.SetMode(1, 1)
# 开启鼠标仿真移动 (解决鼠标不动的问题)
self._wyhkm.SetMode(2, 1) self._wyhkm.SetMode(2, 1)
# 设置推荐的按键/鼠标间隔 (与 test_hw.py 一致30-50ms 更稳定)
self._wyhkm.SetKeyInterval(30, 50) self._wyhkm.SetKeyInterval(30, 50)
self._wyhkm.SetMouseInterval(30, 50) self._wyhkm.SetMouseInterval(30, 50)
print(f">>> [硬件控制] 成功打开设备并完成初始化配置") self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}")
except Exception as e: except Exception as e:
print(f">>> [硬件控制] 初始化异常: {e}") self._log(f">>> [hardware_control] Init failed: {e}")
self._wyhkm = None self._wyhkm = None
def is_available(self): def is_available(self):
if not self._wyhkm: if not self._wyhkm:
return False return False
try: try:
# 修正IsOpen 需要参数 (0:全模式, 1:键盘, 2:鼠标)
# 之前漏传参数会导致 COM 报错,进而导致 key_down 等所有操作被 skip
return self._wyhkm.IsOpen(0) return self._wyhkm.IsOpen(0)
except: except Exception:
return False return False
def delay_rnd(self, min_ms, max_ms): def delay_rnd(self, min_ms, max_ms):
"""随机延时"""
if self.is_available(): if self.is_available():
try: self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) try:
except: pass self._wyhkm.DelayRnd(int(min_ms), int(max_ms))
except Exception:
# --- 键盘操作 (均使用大写字符串) --- pass
def key_down(self, key_str): def key_down(self, key_str):
if self.is_available(): if self.is_available():
try: self._wyhkm.KeyDown(str(key_str).upper()) try:
except: pass self._wyhkm.KeyDown(str(key_str).upper())
except Exception:
pass
def key_up(self, key_str): def key_up(self, key_str):
if self.is_available(): if self.is_available():
try: self._wyhkm.KeyUp(str(key_str).upper()) try:
except: pass self._wyhkm.KeyUp(str(key_str).upper())
except Exception:
pass
def key_press(self, key_str): def key_press(self, key_str):
if self.is_available(): if self.is_available():
try: self._wyhkm.KeyPress(str(key_str).upper()) try:
except: pass self._wyhkm.KeyPress(str(key_str).upper())
except Exception:
pass
# 兼容性别名 def keyDown(self, key_str):
def keyDown(self, key_str): self.key_down(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 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): def move_to(self, x, y):
"""绝对移动"""
if self.is_available(): if self.is_available():
try: self._wyhkm.MoveTo(int(x), int(y)) try:
except: pass 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): def move_r(self, dx, dy):
"""相对移动"""
if self.is_available(): if self.is_available():
try: self._wyhkm.MoveR(int(dx), int(dy)) try:
except: pass 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) self.move_r(dx, dy)
def left_click(self): def left_click(self):
if self.is_available(): if self.is_available():
try: self._wyhkm.LeftClick() try:
except: pass return bool(self._wyhkm.LeftClick())
except Exception as e:
self._log(f">>> [hardware_control] LeftClick failed: {e}")
return False
def right_click(self): def right_click(self):
if self.is_available(): if self.is_available():
try: self._wyhkm.RightClick() try:
except: pass return bool(self._wyhkm.RightClick())
except Exception as e:
self._log(f">>> [hardware_control] RightClick failed: {e}")
return False
def left_down(self): def left_down(self):
if self.is_available(): if self.is_available():
try: self._wyhkm.LeftDown() try:
except: pass return bool(self._wyhkm.LeftDown())
except Exception as e:
self._log(f">>> [hardware_control] LeftDown failed: {e}")
return False
def left_up(self): def left_up(self):
if self.is_available(): if self.is_available():
try: self._wyhkm.LeftUp() try:
except: pass return bool(self._wyhkm.LeftUp())
except Exception as e:
self._log(f">>> [hardware_control] LeftUp failed: {e}")
return False
# 全局实例
hw_ctrl = HardwareController() hw_ctrl = HardwareController()

BIN
images/cursor/Attack.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
images/cursor/LootAll.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Mine.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
images/cursor/Pickup.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
images/cursor/Point.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
images/cursor/REPAIRNPC.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Skin.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
images/cursor/Taxi.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
images/cursor/Trainer.PNG Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

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

View 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
]
]

View 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
]
]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 B

After

Width:  |  Height:  |  Size: 412 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 88 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 B

After

Width:  |  Height:  |  Size: 90 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

After

Width:  |  Height:  |  Size: 85 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 B

After

Width:  |  Height:  |  Size: 92 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 B

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 B

After

Width:  |  Height:  |  Size: 91 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 B

After

Width:  |  Height:  |  Size: 85 B

View File

@@ -583,6 +583,9 @@ class WoWMultiKeyGUI(QMainWindow):
self.init_ui() self.init_ui()
self.find_wow_window() self.find_wow_window()
# 加载参数配置到界面
self._load_params_config()
# 初始化全局热键监听 (F8 用于拾取录制) # 初始化全局热键监听 (F8 用于拾取录制)
self.kb_listener = keyboard.Listener(on_press=self._on_hotkey_press) self.kb_listener = keyboard.Listener(on_press=self._on_hotkey_press)
self.kb_listener.start() self.kb_listener.start()
@@ -1139,7 +1142,6 @@ class WoWMultiKeyGUI(QMainWindow):
self.gs_offset_left.setValue(cfg.get('offset_left', 20)) self.gs_offset_left.setValue(cfg.get('offset_left', 20))
self.gs_offset_top.setValue(cfg.get('offset_top', 45)) 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_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_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_hearthstone_key.setText(str(cfg.get('hearthstone_key', 'b') or 'b'))
self.gs_bag_full_hearthstone.setChecked(bool(cfg.get('bag_full_hearthstone', False))) 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_left'] = self.gs_offset_left.value()
cfg['offset_top'] = self.gs_offset_top.value() cfg['offset_top'] = self.gs_offset_top.value()
cfg['mount_key'] = (self.gs_mount_key.text().strip() or 'x') 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_hold_sec'] = float(self.gs_mount_hold.value())
cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value()) cfg['mount_retry_after_sec'] = float(self.gs_mount_retry.value())
cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b') cfg['hearthstone_key'] = (self.gs_hearthstone_key.text().strip() or 'b')

View File

@@ -11,6 +11,6 @@
"food_key": "f1", "food_key": "f1",
"eat_hp_threshold": 30, "eat_hp_threshold": 30,
"eat_max_wait_sec": 30.0, "eat_max_wait_sec": 30.0,
"enable_mouse_loot": false "enable_mouse_loot": true
} }
} }