diff --git a/Ghostbox_ddl/gbild64.dll b/Ghostbox_ddl/gbild64.dll new file mode 100644 index 0000000..3b12a92 Binary files /dev/null and b/Ghostbox_ddl/gbild64.dll differ diff --git a/Ghostbox_ddl/ghostbox.py b/Ghostbox_ddl/ghostbox.py new file mode 100644 index 0000000..f42a9bd --- /dev/null +++ b/Ghostbox_ddl/ghostbox.py @@ -0,0 +1,260 @@ +# -*- coding: utf-8 -*- +import platform +from ctypes import * +import os + + +# 加载DLL +script_dir = os.path.dirname(os.path.abspath(__file__)) +if platform.architecture()[0] == "64bit": + dll_path = os.path.join(script_dir, "gbild64.dll") +else: + dll_path = os.path.join(script_dir, "gbild32.dll") +dll = windll.LoadLibrary(dll_path) + + +# 设置接口返回值类型 +dll.getmodel.restype = c_char_p +dll.getserialnumber.restype = c_char_p +dll.getproductiondate.restype = c_char_p +dll.getfirmwareversion.restype = c_char_p +dll.getclientscreenresolution.restype = c_char_p +dll.readstring.restype = c_char_p +dll.encryptstring.restype = c_char_p +dll.decryptstring.restype = c_char_p +dll.getproductname.restype = c_char_p +dll.sdktype.restype = c_char_p +dll.sdkversion.restype = c_char_p + + + +# ================ 设备操作 +# 打开设备(根据设备序号) +def opendevice(index=0): + return dll.opendevice(index) +# 打开设备(根据设备ID) +def opendevicebyid(vid,pid): + return dll.opendevicebyid(vid,pid) +# 打开设备(根据设备路径) +def opendevicebypath(path): + return dll.opendevicebypath(bytes(path,"utf-8")) +# 检查设备是否连接 +def isconnected(): + return dll.isconnected() +# 关闭设备 +def closedevice(): + return dll.closedevice() +# 复位设备 +def resetdevice(): + return dll.resetdevice() + + +# ================ 设备信息 +# 获取设备型号 +def getmodel(): + return dll.getmodel().decode("utf-8") +# 获取设备序列号 +def getserialnumber(): + return dll.getserialnumber().decode("utf-8") +# 获取设备生产日期 +def getproductiondate(): + return dll.getproductiondate().decode("utf-8") +# 获取设备固件版本号 +def getfirmwareversion(): + return dll.getfirmwareversion().decode("utf-8") + + +# ================ 键盘操作 +# 按下键 +def presskeybyname(keyn): + return dll.presskeybyname(bytes(keyn,"utf-8")) +def presskeybyvalue(keyv): + return dll.presskeybyvalue(keyv) +# 释放键 +def releasekeybyname(keyn): + return dll.releasekeybyname(bytes(keyn,"utf-8")) +def releasekeybyvalue(keyv): + return dll.releasekeybyvalue(keyv) +# 按下并释放键 +def pressandreleasekeybyname(keyn): + return dll.pressandreleasekeybyname(bytes(keyn,"utf-8")) +def pressandreleasekeybyvalue(keyv): + return dll.pressandreleasekeybyvalue(keyv) +# 判断键盘按键状态 +def iskeypressedbyname(keyn): + return dll.iskeypressedbyname(bytes(keyn,"utf-8")) +def iskeypressedbyvalue(keyv): + return dll.iskeypressedbyvalue(keyv) +# 释放所有键盘按键 +def releaseallkey(): + return dll.releaseallkey() +# 组合按键 +def combinationkey(keys): + return dll.combinationkey(bytes(keys,"utf-8")) +# 输入字符串 +def inputstring(str): + return dll.inputstring(bytes(str,"utf-8")) +# 获取大写锁定状态 +def getcapslock(): + return dll.getcapslock() +# 获取数字键盘锁定状态 +def getnumlock(): + return dll.getnumlock() +# 设置是否区分大小写 +def setcasesensitive(cs): + return dll.setcasesensitive(cs) +# 设置按键延时 +def setpresskeydelay(maxd,mind): + return dll.setpresskeydelay(maxd,mind) +# 设置输入字符串间隔时间 +def setinputstringintervaltime(maxd,mind): + return dll.setinputstringintervaltime(maxd,mind) +# 设置中文输入模式 +def setchineseinputmode(mode): + return dll.setchineseinputmode(mode) + + +# ================ 鼠标操作 +# 按下鼠标键 +def pressmousebutton(mbtn): + return dll.pressmousebutton(mbtn) +# 释放鼠标键 +def releasemousebutton(mbtn): + return dll.releasemousebutton(mbtn) +# 按下并释放鼠标键 +def pressandreleasemousebutton(mbtn): + return dll.pressandreleasemousebutton(mbtn) +# 判断鼠标按键状态 +def ismousebuttonpressed(mbtn): + return dll.ismousebuttonpressed(mbtn) +# 释放所有鼠标按键 +def releaseallmousebutton(): + return dll.releaseallmousebutton() +# 相对移动鼠标 +def movemouserelative(x,y): + return dll.movemouserelative(x,y) +# 移动鼠标到指定坐标 +def movemouseto(x,y): + return dll.movemouseto(x,y) +# 获取鼠标当前位置 +def getmousex(): + return dll.getmousex() +def getmousey(): + return dll.getmousey() +# 移动鼠标滚轮 +def movemousewheel(z): + return dll.movemousewheel(z) +# 设置鼠标按键延时 +def setpressmousebuttondelay(maxd,mind): + return dll.setpressmousebuttondelay(maxd,mind) +# 设置鼠标移动延时 +def setmousemovementdelay(maxd,mind): + return dll.setmousemovementdelay(maxd,mind) +# 设置鼠标移动速度 +def setmousemovementspeed(speedvalue): + return dll.setmousemovementspeed(speedvalue) +# 设置鼠标移动模式 +def setmousemovementmode(modevalue): + return dll.setmousemovementmode(modevalue) + + +# ================ 双机设备接口 +# 设置鼠标当前位置(针对不支持绝对值的鼠标) +def setmouseposition(x,y): + return dll.setmouseposition(x,y) +# 设置鼠标绝对位置(针对支持绝对值的鼠标) +def setmouseabsoluteposition(x,y): + return dll.setmouseabsoluteposition(x,y) +# 设置鼠标逻辑位置(不移动鼠标,只更新内部当前坐标) +def setmouselogicalposition(x,y): + return dll.setmouselogicalposition(x,y) +# 相对移动鼠标(只移动鼠标,不更新内部当前坐标) +def movemouserelative_ncc(x,y): + return dll.movemouserelative_ncc(x,y) +# 设置被控端屏幕分辨率 +def setclientscreenresolution(width,height): + return dll.setclientscreenresolution(width,height) +# 获取被控端屏幕分辨率 +def getclientscreenresolution(): + return dll.getclientscreenresolution().decode("utf-8") +# 选择被控端(对于APV系列的一控多模式,指定键盘鼠标操作对应的设备) +def selectsubdevice(addr): + return dll.selectsubdevice(addr) +# 修改被控端设备ID devtype: 0 主控端,1 被控端键盘(U2),2 被控端鼠标(U2),3 被控端复合键鼠(U1) +def setslavedeviceid(vid,pid,devtype): + return dll.setslavedeviceid(vid,pid,devtype) + + +# ================ 加密狗操作 +# 初始化加密狗 +def initializedongle(): + return dll.initializedongle() +# 设置读密码 +def setreadpassword(writepwd,newpwd): + return dll.setreadpassword(bytes(writepwd,"utf-8"),bytes(newpwd,"utf-8")) +# 设置写密码 +def setwritepassword(oldpwd,newpwd): + return dll.setwritepassword(bytes(oldpwd,"utf-8"),bytes(newpwd,"utf-8")) +# 从设备读字符串 +def readstring(readpwd,addr,count): + return dll.readstring(bytes(readpwd,"utf-8"),addr,count).decode("utf-8", errors="ignore") +# 将字符串写入设备 +def writestring(writepwd,str,addr): + return dll.writestring(bytes(writepwd,"utf-8"),bytes(str,"utf-8"),addr) +# 设置密钥 +def setencryptionkey(writepwd,cipher): + return dll.setencryptionkey(bytes(writepwd,"utf-8"),bytes(cipher,"utf-8")) +# 加密字符串 +def encryptstring(str): + return dll.encryptstring(bytes(str,"utf-8")).decode("utf-8") +# 解密字符串 +def decryptstring(str): + return dll.decryptstring(bytes(str,"utf-8")).decode("utf-8") + + +# ================ 电源控制接口 +# 按下电源按钮 +def presspowerbutton(): + return dll.presspowerbutton() +# 释放电源按钮 +def releasepowerbutton(): + return dll.releasepowerbutton() +# 按下并释放电源按钮 +def pressandreleasepowerbutton(): + return dll.pressandreleasepowerbutton() +# 获取电源工作状态 +def getpowerstatus(): + return dll.getpowerstatus() + + +# ================ 设备定义接口 +# 修改设备速度 +def setspeed(speed): + return dll.setspeed() +# 修改设备ID +def setdeviceid(vid,pid): + return dll.setdeviceid(vid,pid) +# 恢复设备默认ID +def restoredeviceid(): + return dll.restoredeviceid() +# 获取当前设备VID +def getvid(): + return dll.getvid() +# 获取当前设备PID +def getpid(): + return dll.getpid() +# 设置产品名称 +def setproductname(name): + return dll.setproductname(bytes(name,"gbk")) +# 获取产品名称 +def getproductname(): + return dll.getproductname().decode("gbk") + + +# ================ 其他操作 +# 获取当前SDK库类型 +def sdktype(): + return dll.sdktype().decode("utf-8") +# 获取当前SDK库版本 +def sdkversion(): + return dll.sdkversion().decode("utf-8") diff --git a/Ghostbox_ddl/test_ghostbox.py b/Ghostbox_ddl/test_ghostbox.py new file mode 100644 index 0000000..b9e3f5e --- /dev/null +++ b/Ghostbox_ddl/test_ghostbox.py @@ -0,0 +1,31 @@ +# -*- coding: utf-8 -*- +import ghostbox as gb +import time + +print(f"{gb.sdktype()}({gb.sdkversion()})") + +# 打开设备 +print(gb.opendevice()) +#print(gb.opendevicebyid(0x5188,0x1801)) +#print(gb.opendevicebypath("\\\\?\\hid#vid_5188&pid_1801&mi_02#7&28610698&0&0000#{4d1e55b2-f16f-11cf-88cb-001111000030}")) +# 获取设备型号 +print(gb.getmodel()) +# 获取硬件版本号 +print(gb.getfirmwareversion()) +# 按win键 +# print(gb.pressandreleasekeybyname("win")) +# 移动鼠标 +# gb.setmousemovementmode(3) +# gb.setmousemovementdelay(3,5) +# print(gb.movemouseto(100,100)) + +gb.presskeybyname("right") +gb.presskeybyname("up") +time.sleep(3) +gb.releaseallkey() + +# gb.combinationkey("win+r") + +# time.sleep(2) +# print(gb.getcapslock()) +# gb.inputstring("xxxDDDff") \ No newline at end of file diff --git a/auto_bot_move.py b/auto_bot_move.py index 3590d23..e16a7de 100644 --- a/auto_bot_move.py +++ b/auto_bot_move.py @@ -287,6 +287,7 @@ class AutoBotMove: ) self.distance_interact_brake_sec = 0.08 self._last_mouse_path_scale_signature = None + self.logistics_resume_cooldown_until = 0.0 # stop_check: 返回 True 表示需要立即停止(用于中断阻塞中的后勤/路线导航) self._stop_check = stop_check if callable(stop_check) else (lambda: False) if waypoints is None: @@ -327,6 +328,12 @@ class AutoBotMove: self.logistics_manager = LogisticsManager(vendor_file) self.logistics_manager.bag_full_hearthstone = bool(layout.get("bag_full_hearthstone", False)) self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b") + self.logistics_manager.hearthstone_cast_sec = float(layout.get("hearthstone_wait_sec", 12.0)) + self.logistics_manager.logistics_full_auto = bool(layout.get("logistics_full_auto", False)) + self.logistics_manager.bag_slot_threshold = int(layout.get("bag_slot_threshold", 2)) + self.logistics_manager.durability_threshold = float(layout.get("durability_threshold", 0.2)) + self.logistics_manager.repair_target_key = str(layout.get("repair_target_key", "8") or "8") + self.logistics_manager.repair_interact_key = str(layout.get("repair_interact_key", "4") or "4") self.logistics_manager.enable_bag_full_mail = bool(layout.get("enable_bag_full_mail", False)) self.logistics_manager.mailbox_route_file = str( mailbox_route_path @@ -342,6 +349,25 @@ class AutoBotMove: def _has_prepare_route(self) -> bool: return bool(self.prepare_route_waypoints) + def _reset_prepare_route_for_logistics_resume(self): + self.prepare_route_index = 0 + self.prepare_route_completed = not bool(self.prepare_route_waypoints) + self.is_running_prepare_route = bool(self.prepare_route_waypoints) + self.prepare_route_snapped = False + self.target_acquired_time = None + self.last_turn_signal_time = 0.0 + self.last_interaction_time = 0.0 + self.distance_interact_brake_pending = False + self.distance_interact_pause_until = 0.0 + self.logistics_resume_cooldown_until = time.time() + 5.0 + self.last_tab_time = time.time() + 1.0 + self.patrol_controller.stop_all() + self.patrol_controller.reset_stuck() + if self.prepare_route_waypoints: + print(">>> [后勤] 全自动续跑:重置准备路线,准备返回巡逻。") + else: + print(">>> [后勤] 全自动续跑:未配置准备路线,直接恢复巡逻。") + def _snap_prepare_route_to_nearest(self, state): if self.prepare_route_snapped or self.prepare_route_completed or not self._has_prepare_route(): return @@ -768,40 +794,46 @@ class AutoBotMove: self.death_manager.run_to_corpse(state, get_state) return - # 2. 后勤检查(脱战时):空格或耐久不足则回城 - self.logistics_manager.check_logistics(state) + effective_target = self._is_effective_target(state) + in_combat_or_target = bool(state['combat'] or effective_target) + + # 2. 后勤检查:只在脱战且无有效目标时触发,避免战斗中突然回城/修理/邮寄。 + if not in_combat_or_target and time.time() >= self.logistics_resume_cooldown_until: + self.logistics_manager.check_logistics(state) + else: + self.logistics_manager.is_returning = False + if self.logistics_manager.is_returning: if self.is_moving: self.patrol_controller.stop_all() self.is_moving = False self.patrol_controller.reset_stuck() - bag_full_now = int(state.get('free_slots', 0) or 0) < 2 - if bag_full_now and self.logistics_manager.enable_bag_full_mail: - get_state_fn = (lambda: None if self._should_stop() else parse_game_state()) - self.logistics_manager.run_bag_full_mail_flow( - get_state_fn, - self.patrol_controller, - stop_check=self._should_stop, - ) - if callable(getattr(self, '_on_hearthstone_stop', None)): - self._on_hearthstone_stop() + + tasks = self.logistics_manager.get_pending_tasks(state) + get_state_fn = (lambda: None if self._should_stop() else parse_game_state()) + ok, completed_tasks = self.logistics_manager.run_pending_tasks( + tasks, + get_state_fn, + self.patrol_controller, + stop_check=self._should_stop, + ) + + can_resume = ( + ok + and self.logistics_manager.logistics_full_auto + and any(task in ("mail", "repair") for task in completed_tasks) + ) + if can_resume: + self._reset_prepare_route_for_logistics_resume() + self.is_moving = False return - # 勾选"包满炉石回城":只有真正包满时才炉石;耐久低仍走修理路线 - if bag_full_now and self.logistics_manager.bag_full_hearthstone: - get_state_fn = (lambda: None if self._should_stop() else parse_game_state()) - self.logistics_manager.use_hearthstone_and_stop(get_state=get_state_fn) - if callable(getattr(self, '_on_hearthstone_stop', None)): - self._on_hearthstone_stop() - return - # 中断策略:一旦 GUI 停止,后续 get_state 返回 None,使 navigate_path 立即退出。 - get_state = (lambda: None if self._should_stop() else parse_game_state()) - self.logistics_manager.run_route1_round(get_state, self.patrol_controller) + + if callable(getattr(self, '_on_hearthstone_stop', None)): + self._on_hearthstone_stop() return # 3. 战斗/有目标:停止移动,执行攻击逻辑;仅在「跑向怪」短窗口内做卡死检测 - effective_target = self._is_effective_target(state) # 核心修改:只要还在战斗中,就不算“完全脱战”,即使当前没目标(可能正在 Tab 找下一个) - in_combat_or_target = bool(state['combat'] or effective_target) if in_combat_or_target: # 被动进战时立即打断进食等待,转入正常战斗流程 diff --git a/build.spec b/build.spec index 2f1f24c..9d2c9f0 100644 --- a/build.spec +++ b/build.spec @@ -6,6 +6,7 @@ added_files = [ ('recorder\\*.json', 'recorder'), ('game_state_config.json', '.'), ('ddl', 'ddl'), + ('Ghostbox_ddl', 'Ghostbox_ddl'), ('images', 'images'), ('loot_path.json', '.'), ] diff --git a/build_exe.ps1 b/build_exe.ps1 index 0edee96..6d839fa 100644 --- a/build_exe.ps1 +++ b/build_exe.ps1 @@ -28,6 +28,10 @@ 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 Ghostbox_ddl folder to dist..." -ForegroundColor Cyan +if (Test-Path "dist\Ghostbox_ddl") { Remove-Item "dist\Ghostbox_ddl" -Recurse -Force -ErrorAction SilentlyContinue } +if (Test-Path "Ghostbox_ddl") { Copy-Item "Ghostbox_ddl" "dist\Ghostbox_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 } diff --git a/build_wow_multikey.spec b/build_wow_multikey.spec index bae1ad1..34d5788 100644 --- a/build_wow_multikey.spec +++ b/build_wow_multikey.spec @@ -6,6 +6,7 @@ added_files = [ ('recorder\\*.json', 'recorder'), ('game_state_config.json', '.'), ('ddl', 'ddl'), + ('Ghostbox_ddl', 'Ghostbox_ddl'), ('images', 'images'), ('loot_path.json', '.'), ] diff --git a/coordinate_patrol.py b/coordinate_patrol.py index 02d81e0..de4f766 100644 --- a/coordinate_patrol.py +++ b/coordinate_patrol.py @@ -490,7 +490,14 @@ class CoordinatePatrol: return False - def navigate_path(self, get_state, path, forward=True, arrival_threshold=None): + def navigate_path( + self, + get_state, + path, + forward=True, + arrival_threshold=None, + snap_to_nearest=False, + ): """ 按 path 依次走完所有点后返回。每次调用都必须传入 path。 get_state: 可调用对象,每次调用返回当前状态 dict,需包含 'x','y','facing'。 @@ -520,6 +527,26 @@ class CoordinatePatrol: break time.sleep(poll_sleep_sec) + if snap_to_nearest and points: + current_pos = None + try: + current_pos = (float(state.get("x")), float(state.get("y"))) + except Exception: + current_pos = None + if current_pos is not None: + best_idx = 0 + best_dist = None + for idx, point in enumerate(points): + dist = self.get_distance(current_pos, point) + if best_dist is None or dist < best_dist: + best_idx = idx + best_dist = dist + if best_idx > 0: + self.logger.info( + f">>> 路径智能接入最近点 {best_idx + 1}/{len(points)},距离 {best_dist:.2f}" + ) + points = points[best_idx:] + for i, target in enumerate(points): self.logger.info(f">>> 路径点 {i + 1}/{len(points)}: {target}") while True: diff --git a/game_state.py b/game_state.py index e3b9f89..ff9ca45 100644 --- a/game_state.py +++ b/game_state.py @@ -30,7 +30,13 @@ _DEFAULTS = { "mount_retry_after_sec": 2.0, # 炉石回城 "hearthstone_key": "b", + "hearthstone_wait_sec": 12.0, "bag_full_hearthstone": False, + "logistics_full_auto": False, + "bag_slot_threshold": 2, + "durability_threshold": 0.2, + "repair_target_key": "8", + "repair_interact_key": "4", # 包满炉石后跑邮箱,并触发 MailboxCourier 插件宏 "enable_bag_full_mail": False, "mailbox_route_json": "recorder/mailbox.json", diff --git a/game_state_config.json b/game_state_config.json index fa6bf87..119dc58 100644 --- a/game_state_config.json +++ b/game_state_config.json @@ -12,5 +12,18 @@ "mount_hold_sec": 2.0, "mount_retry_after_sec": 2.0, "hearthstone_key": "6", - "bag_full_hearthstone": true + "hearthstone_wait_sec": 12.0, + "bag_full_hearthstone": true, + "logistics_full_auto": false, + "bag_slot_threshold": 2, + "durability_threshold": 0.2, + "repair_target_key": "8", + "repair_interact_key": "4", + "enable_bag_full_mail": false, + "mailbox_route_json": "recorder/mailbox.json", + "mailbox_interact_key": "4", + "mail_recipient_key": "f7", + "mail_send_key": "f8", + "mailbox_open_wait_sec": 2.0, + "mail_send_wait_sec": 60.0 } diff --git a/hardware_control.py b/hardware_control.py index 2cc94af..e00bd69 100644 --- a/hardware_control.py +++ b/hardware_control.py @@ -1,3 +1,4 @@ +import importlib.util import json import os import random @@ -20,35 +21,489 @@ else: MAIN_CONFIG_FILE = "wow_multikey_qt.json" +BACKEND_TIANYA = "tianya" +BACKEND_GHOST = "ghost" +BACKEND_DIRECTINPUT = "directinput" +TIANYA_DLL_PATH = "ddl/wyhkm.dll" +GHOSTBOX_MODULE_PATH = "Ghostbox_ddl/ghostbox.py" + + +class _TianyaBackend: + name = BACKEND_TIANYA + + def __init__(self, controller, dll_path=TIANYA_DLL_PATH): + self._controller = controller + self._dll_path_setting = dll_path + self._wyhkm = None + self.dll_path = None + + def configure(self, dll_path=None): + if dll_path: + self._dll_path_setting = dll_path + + def label(self): + return self.name if self.is_available() else f"{self.name}_unavailable" + + def is_available(self): + if self._wyhkm: + try: + if self._wyhkm.IsOpen(0): + return True + except Exception: + self._wyhkm = None + + self.dll_path = self._controller._resolve_resource_path(self._dll_path_setting) + if not self.dll_path: + self._controller._log(f">>> [tianya_control] DLL not found: {self._dll_path_setting}") + self._wyhkm = None + return False + + try: + hkmdll = windll.LoadLibrary(self.dll_path) + hkmdll.DllInstall.argtypes = (c_long, c_longlong) + + if hkmdll.DllInstall(1, 2) < 0: + self._controller._log(">>> [tianya_control] DllInstall failed") + self._wyhkm = None + return False + + pythoncom.CoInitialize() + self._wyhkm = win32com.client.Dispatch("wyp.hkm") + + dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0) + if dev_id == -1: + self._controller._log(">>> [tianya_control] Device not found (Index 0)") + self._wyhkm = None + return False + + if not self._wyhkm.Open(dev_id, 0): + self._controller._log(">>> [tianya_control] Open device failed") + self._wyhkm = None + return False + + self._wyhkm.SetMode(1, 1) + self._wyhkm.SetMode(2, 1) + self._wyhkm.SetKeyInterval(30, 50) + self._wyhkm.SetMouseInterval(30, 50) + + self._controller._log(f">>> [tianya_control] Device opened and initialized: {dev_id}") + return True + except Exception as exc: + self._controller._log(f">>> [tianya_control] Init failed: {exc}") + self._wyhkm = None + return False + + def _normalize_key(self, key_str): + return str(key_str).strip().upper() + + def delay_rnd(self, min_ms, max_ms): + if not self.is_available(): + return False + try: + self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) + return True + except Exception: + return False + + def key_down(self, key_str): + if not self.is_available(): + return False + try: + self._wyhkm.KeyDown(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [tianya_control] KeyDown failed ({key_str}): {exc}") + return False + + def key_up(self, key_str): + if not self.is_available(): + return False + try: + self._wyhkm.KeyUp(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [tianya_control] KeyUp failed ({key_str}): {exc}") + return False + + def key_press(self, key_str): + if not self.is_available(): + return False + try: + self._wyhkm.KeyPress(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [tianya_control] KeyPress failed ({key_str}): {exc}") + return False + + def move_to(self, x, y): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.MoveTo(int(x), int(y))) + except Exception as exc: + self._controller._log(f">>> [tianya_control] MoveTo failed ({x}, {y}): {exc}") + return False + + def move_r(self, dx, dy): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.MoveR(int(dx), int(dy))) + except Exception as exc: + self._controller._log(f">>> [tianya_control] MoveR failed ({dx}, {dy}): {exc}") + return False + + def left_click(self): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.LeftClick()) + except Exception as exc: + self._controller._log(f">>> [tianya_control] LeftClick failed: {exc}") + return False + + def right_click(self): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.RightClick()) + except Exception as exc: + self._controller._log(f">>> [tianya_control] RightClick failed: {exc}") + return False + + def left_down(self): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.LeftDown()) + except Exception as exc: + self._controller._log(f">>> [tianya_control] LeftDown failed: {exc}") + return False + + def left_up(self): + if not self.is_available(): + return False + try: + return bool(self._wyhkm.LeftUp()) + except Exception as exc: + self._controller._log(f">>> [tianya_control] LeftUp failed: {exc}") + return False + + +class _GhostboxBackend: + name = BACKEND_GHOST + LEFT_BUTTON = 1 + RIGHT_BUTTON = 2 + + def __init__(self, controller, module_path=GHOSTBOX_MODULE_PATH): + self._controller = controller + self._module_path_setting = module_path + self._module = None + self.module_path = None + + def configure(self, module_path=None): + if module_path and module_path != self._module_path_setting: + self._module_path_setting = module_path + self._module = None + + def label(self): + return self.name if self.is_available() else f"{self.name}_unavailable" + + def _load_module(self): + if self._module is not None: + return self._module + + self.module_path = self._controller._resolve_resource_path(self._module_path_setting) + if not self.module_path: + self._controller._log(f">>> [ghost_control] module not found: {self._module_path_setting}") + return None + + try: + spec = importlib.util.spec_from_file_location("ghostbox_runtime", self.module_path) + if spec is None or spec.loader is None: + self._controller._log(f">>> [ghost_control] load spec failed: {self.module_path}") + return None + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + self._module = module + return module + except Exception as exc: + self._controller._log(f">>> [ghost_control] load failed: {exc}") + self._module = None + return None + + def is_available(self): + gb = self._load_module() + if gb is None: + return False + + try: + if gb.isconnected(): + return True + except Exception: + pass + + try: + result = gb.opendevice() + connected = bool(gb.isconnected()) + if not connected: + self._controller._log(f">>> [ghost_control] Open device failed: result={result}") + return False + + try: + gb.setpresskeydelay(30, 50) + gb.setpressmousebuttondelay(30, 50) + gb.setmousemovementdelay(3, 5) + except Exception: + pass + + self._controller._log(">>> [ghost_control] Device opened and initialized") + return True + except Exception as exc: + self._controller._log(f">>> [ghost_control] Init failed: {exc}") + return False + + def _normalize_key(self, key_str): + return str(key_str).strip().lower() + + def _call(self, action, func, *args): + if not self.is_available(): + return False + try: + func(*args) + return True + except Exception as exc: + self._controller._log(f">>> [ghost_control] {action} failed: {exc}") + return False + + def delay_rnd(self, min_ms, max_ms): + low = min(int(min_ms), int(max_ms)) + high = max(int(min_ms), int(max_ms)) + time.sleep(random.uniform(low, high) / 1000.0) + return True + + def key_down(self, key_str): + gb = self._load_module() + if gb is None: + return False + return self._call(f"KeyDown({key_str})", gb.presskeybyname, self._normalize_key(key_str)) + + def key_up(self, key_str): + gb = self._load_module() + if gb is None: + return False + return self._call(f"KeyUp({key_str})", gb.releasekeybyname, self._normalize_key(key_str)) + + def key_press(self, key_str): + gb = self._load_module() + if gb is None: + return False + return self._call(f"KeyPress({key_str})", gb.pressandreleasekeybyname, self._normalize_key(key_str)) + + def move_to(self, x, y): + gb = self._load_module() + if gb is None: + return False + return self._call(f"MoveTo({x}, {y})", gb.movemouseto, int(x), int(y)) + + def move_r(self, dx, dy): + gb = self._load_module() + if gb is None: + return False + return self._call(f"MoveR({dx}, {dy})", gb.movemouserelative, int(dx), int(dy)) + + def left_click(self): + gb = self._load_module() + if gb is None: + return False + return self._call("LeftClick", gb.pressandreleasemousebutton, self.LEFT_BUTTON) + + def right_click(self): + gb = self._load_module() + if gb is None: + return False + return self._call("RightClick", gb.pressandreleasemousebutton, self.RIGHT_BUTTON) + + def left_down(self): + gb = self._load_module() + if gb is None: + return False + return self._call("LeftDown", gb.pressmousebutton, self.LEFT_BUTTON) + + def left_up(self): + gb = self._load_module() + if gb is None: + return False + return self._call("LeftUp", gb.releasemousebutton, self.LEFT_BUTTON) + + +class _DirectInputBackend: + name = BACKEND_DIRECTINPUT + + def __init__(self, controller): + self._controller = controller + self._last_error = None + + def label(self): + return self.name if self.is_available() else f"{self.name}_unavailable" + + def is_available(self): + if pydirectinput is not None: + return True + if self._last_error != _PYDIRECTINPUT_IMPORT_ERROR: + self._last_error = _PYDIRECTINPUT_IMPORT_ERROR + self._controller._log(f">>> [input_control] pydirectinput unavailable: {_PYDIRECTINPUT_IMPORT_ERROR}") + return False + + def _normalize_key(self, key_str): + return str(key_str).strip().lower() + + def delay_rnd(self, min_ms, max_ms): + if not self.is_available(): + return False + low = min(int(min_ms), int(max_ms)) + high = max(int(min_ms), int(max_ms)) + time.sleep(random.uniform(low, high) / 1000.0) + return True + + def key_down(self, key_str): + if not self.is_available(): + return False + try: + pydirectinput.keyDown(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [directinput] keyDown failed ({key_str}): {exc}") + return False + + def key_up(self, key_str): + if not self.is_available(): + return False + try: + pydirectinput.keyUp(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [directinput] keyUp failed ({key_str}): {exc}") + return False + + def key_press(self, key_str): + if not self.is_available(): + return False + try: + pydirectinput.press(self._normalize_key(key_str)) + return True + except Exception as exc: + self._controller._log(f">>> [directinput] press failed ({key_str}): {exc}") + return False + + def move_to(self, x, y): + if not self.is_available(): + return False + try: + pydirectinput.moveTo(int(x), int(y)) + return True + except Exception as exc: + self._controller._log(f">>> [directinput] moveTo failed ({x}, {y}): {exc}") + return False + + def move_r(self, dx, dy): + if not self.is_available(): + return False + try: + pydirectinput.moveRel(int(dx), int(dy)) + return True + except Exception as exc: + self._controller._log(f">>> [directinput] moveRel failed ({dx}, {dy}): {exc}") + return False + + def left_click(self): + if not self.is_available(): + return False + try: + pydirectinput.click(button="left") + return True + except Exception as exc: + self._controller._log(f">>> [directinput] left click failed: {exc}") + return False + + def right_click(self): + if not self.is_available(): + return False + try: + pydirectinput.click(button="right") + return True + except Exception as exc: + self._controller._log(f">>> [directinput] right click failed: {exc}") + return False + + def left_down(self): + if not self.is_available(): + return False + try: + pydirectinput.mouseDown(button="left") + return True + except Exception as exc: + self._controller._log(f">>> [directinput] left mouseDown failed: {exc}") + return False + + def left_up(self): + if not self.is_available(): + return False + try: + pydirectinput.mouseUp(button="left") + return True + except Exception as exc: + self._controller._log(f">>> [directinput] left mouseUp failed: {exc}") + return False class HardwareController: _instance = None - _wyhkm = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super(HardwareController, cls).__new__(cls) return cls._instance - def __init__(self, dll_path="ddl/wyhkm.dll", use_hardware_input=None): + def __init__( + self, + dll_path=TIANYA_DLL_PATH, + use_hardware_input=None, + use_tianya_box=None, + use_ghost_box=None, + ghostbox_module_path=GHOSTBOX_MODULE_PATH, + backend=None, + ): if getattr(self, "_bootstrapped", False): - if dll_path: - self._dll_path_setting = dll_path - if use_hardware_input is not None: - self.configure(use_hardware_input=use_hardware_input, dll_path=dll_path) + self._tianya_backend.configure(dll_path=dll_path) + self._ghost_backend.configure(module_path=ghostbox_module_path) + if backend is not None or use_hardware_input is not None or use_tianya_box is not None or use_ghost_box is not None: + self.configure( + use_hardware_input=use_hardware_input, + use_tianya_box=use_tianya_box, + use_ghost_box=use_ghost_box, + dll_path=dll_path, + ghostbox_module_path=ghostbox_module_path, + backend=backend, + ) return self._bootstrapped = True - self._dll_path_setting = dll_path - self._backend = "hardware" + self._backend = BACKEND_TIANYA self._last_backend_log = None - self._last_directinput_error = None - self.dll_path = None + self._tianya_backend = _TianyaBackend(self, dll_path) + self._ghost_backend = _GhostboxBackend(self, ghostbox_module_path) + self._directinput_backend = _DirectInputBackend(self) - if use_hardware_input is None: - use_hardware_input = self._load_backend_preference(default=True) - self.configure(use_hardware_input=use_hardware_input, dll_path=dll_path) + if backend is None and use_hardware_input is None and use_tianya_box is None and use_ghost_box is None: + use_tianya_box, use_ghost_box = self._load_backend_preference(default_tianya=True) + self.configure( + use_hardware_input=use_hardware_input, + use_tianya_box=use_tianya_box, + use_ghost_box=use_ghost_box, + backend=backend, + ) def _runtime_base_dir(self): if getattr(sys, "frozen", False): @@ -100,18 +555,21 @@ class HardwareController: return candidate return os.path.join(self._runtime_base_dir(), MAIN_CONFIG_FILE) - def _load_backend_preference(self, default=True): + def _load_backend_preference(self, default_tianya=True): config_path = self._config_path() if not os.path.exists(config_path): - return default + return default_tianya, False try: with open(config_path, "r", encoding="utf-8") as f: cfg = json.load(f) bot_cfg = (cfg or {}).get("bot") or {} - return bool(bot_cfg.get("use_hardware_input", default)) + has_new_flags = "use_tianya_box" in bot_cfg or "use_ghost_box" in bot_cfg + if has_new_flags: + return bool(bot_cfg.get("use_tianya_box", False)), bool(bot_cfg.get("use_ghost_box", False)) + return bool(bot_cfg.get("use_hardware_input", default_tianya)), False except Exception as exc: self._log(f">>> [input_control] load config failed: {exc}") - return default + return default_tianya, False def _log(self, message): print(message) @@ -122,68 +580,28 @@ class HardwareController: except Exception: pass - def _normalize_hardware_key(self, key_str): - return str(key_str).strip().upper() + def _normalize_backend(self, backend): + if backend in (BACKEND_TIANYA, "hardware"): + return BACKEND_TIANYA + if backend == BACKEND_GHOST: + return BACKEND_GHOST + if backend == BACKEND_DIRECTINPUT: + return BACKEND_DIRECTINPUT + return BACKEND_DIRECTINPUT - def _normalize_directinput_key(self, key_str): - return str(key_str).strip().lower() + def _select_backend(self, use_tianya_box, use_ghost_box): + if bool(use_tianya_box): + return BACKEND_TIANYA + if bool(use_ghost_box): + return BACKEND_GHOST + return BACKEND_DIRECTINPUT - def _directinput_available(self): - if pydirectinput is not None: - return True - if self._last_directinput_error != _PYDIRECTINPUT_IMPORT_ERROR: - self._last_directinput_error = _PYDIRECTINPUT_IMPORT_ERROR - self._log(f">>> [input_control] pydirectinput unavailable: {_PYDIRECTINPUT_IMPORT_ERROR}") - return False - - def _ensure_hardware_ready(self): - if self._wyhkm: - try: - if self._wyhkm.IsOpen(0): - return True - except Exception: - self._wyhkm = None - - self.dll_path = self._resolve_resource_path(self._dll_path_setting) - if not self.dll_path: - self._log(f">>> [hardware_control] DLL not found: {self._dll_path_setting}") - self._wyhkm = None - return False - - try: - hkmdll = windll.LoadLibrary(self.dll_path) - hkmdll.DllInstall.argtypes = (c_long, c_longlong) - - if hkmdll.DllInstall(1, 2) < 0: - self._log(">>> [hardware_control] DllInstall failed") - self._wyhkm = None - return False - - pythoncom.CoInitialize() - self._wyhkm = win32com.client.Dispatch("wyp.hkm") - - dev_id = self._wyhkm.SearchDevice(0x2612, 0x1701, 0) - if dev_id == -1: - self._log(">>> [hardware_control] Device not found (Index 0)") - self._wyhkm = None - return False - - if not self._wyhkm.Open(dev_id, 0): - self._log(">>> [hardware_control] Open device failed") - self._wyhkm = None - return False - - self._wyhkm.SetMode(1, 1) - self._wyhkm.SetMode(2, 1) - self._wyhkm.SetKeyInterval(30, 50) - self._wyhkm.SetMouseInterval(30, 50) - - self._log(f">>> [hardware_control] Device opened and initialized: {dev_id}") - return True - except Exception as exc: - self._log(f">>> [hardware_control] Init failed: {exc}") - self._wyhkm = None - return False + def _current_backend(self): + if self._backend == BACKEND_TIANYA: + return self._tianya_backend + if self._backend == BACKEND_GHOST: + return self._ghost_backend + return self._directinput_backend def _log_backend(self): label = self.backend_label() @@ -191,92 +609,62 @@ class HardwareController: self._last_backend_log = label self._log(f">>> [input_control] backend={label}") - def configure(self, use_hardware_input=True, dll_path=None): - if dll_path: - self._dll_path_setting = dll_path - self._backend = "hardware" if bool(use_hardware_input) else "directinput" - if self._backend == "hardware": - self._ensure_hardware_ready() + def configure( + self, + use_hardware_input=None, + use_tianya_box=None, + use_ghost_box=None, + dll_path=None, + ghostbox_module_path=None, + backend=None, + ): + self._tianya_backend.configure(dll_path=dll_path) + self._ghost_backend.configure(module_path=ghostbox_module_path) + + if backend is not None: + self._backend = self._normalize_backend(backend) else: - self._directinput_available() + if use_tianya_box is None and use_ghost_box is None: + if use_hardware_input is None: + use_tianya_box, use_ghost_box = self._load_backend_preference(default_tianya=True) + else: + use_tianya_box, use_ghost_box = bool(use_hardware_input), False + else: + use_tianya_box = bool(use_tianya_box) + use_ghost_box = bool(use_ghost_box) + if use_hardware_input is not None and not bool(use_hardware_input): + use_tianya_box, use_ghost_box = False, False + self._backend = self._select_backend(use_tianya_box, use_ghost_box) + self._log_backend() return self._backend def uses_hardware_input(self): - return self._backend == "hardware" + return self._backend in (BACKEND_TIANYA, BACKEND_GHOST) + + def uses_tianya_box(self): + return self._backend == BACKEND_TIANYA + + def uses_ghost_box(self): + return self._backend == BACKEND_GHOST def backend_label(self): - if self.uses_hardware_input(): - return "hardware" if self._ensure_hardware_ready() else "hardware_unavailable" - return "directinput" if self._directinput_available() else "directinput_unavailable" + return self._current_backend().label() def is_available(self): - if self.uses_hardware_input(): - return self._ensure_hardware_ready() - return self._directinput_available() + return self._current_backend().is_available() def delay_rnd(self, min_ms, max_ms): - if self.uses_hardware_input() and self.is_available(): - try: - self._wyhkm.DelayRnd(int(min_ms), int(max_ms)) - except Exception: - pass - return - if self._directinput_available(): - low = min(int(min_ms), int(max_ms)) - high = max(int(min_ms), int(max_ms)) - time.sleep(random.uniform(low, high) / 1000.0) + return self._current_backend().delay_rnd(min_ms, max_ms) def key_down(self, key_str): - if self.uses_hardware_input(): - if self.is_available(): - try: - self._wyhkm.KeyDown(self._normalize_hardware_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [hardware_control] KeyDown failed ({key_str}): {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.keyDown(self._normalize_directinput_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [directinput] keyDown failed ({key_str}): {exc}") - return False + return self._current_backend().key_down(key_str) def key_up(self, key_str): - if self.uses_hardware_input(): - if self.is_available(): - try: - self._wyhkm.KeyUp(self._normalize_hardware_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [hardware_control] KeyUp failed ({key_str}): {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.keyUp(self._normalize_directinput_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [directinput] keyUp failed ({key_str}): {exc}") - return False + return self._current_backend().key_up(key_str) def key_press(self, key_str): - if self.uses_hardware_input(): - if self.is_available(): - try: - self._wyhkm.KeyPress(self._normalize_hardware_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [hardware_control] KeyPress failed ({key_str}): {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.press(self._normalize_directinput_key(key_str)) - return True - except Exception as exc: - self._log(f">>> [directinput] press failed ({key_str}): {exc}") - return False + return self._current_backend().key_press(key_str) def keyDown(self, key_str): return self.key_down(key_str) @@ -288,103 +676,25 @@ class HardwareController: return self.key_press(key_str) def move_to(self, x, y): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.MoveTo(int(x), int(y))) - except Exception as exc: - self._log(f">>> [hardware_control] MoveTo failed ({x}, {y}): {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.moveTo(int(x), int(y)) - return True - except Exception as exc: - self._log(f">>> [directinput] moveTo failed ({x}, {y}): {exc}") - return False + return self._current_backend().move_to(x, y) def move_r(self, dx, dy): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.MoveR(int(dx), int(dy))) - except Exception as exc: - self._log(f">>> [hardware_control] MoveR failed ({dx}, {dy}): {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.moveRel(int(dx), int(dy)) - return True - except Exception as exc: - self._log(f">>> [directinput] moveRel failed ({dx}, {dy}): {exc}") - return False + return self._current_backend().move_r(dx, dy) def MoveR(self, dx, dy): return self.move_r(dx, dy) def left_click(self): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.LeftClick()) - except Exception as exc: - self._log(f">>> [hardware_control] LeftClick failed: {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.click(button="left") - return True - except Exception as exc: - self._log(f">>> [directinput] left click failed: {exc}") - return False + return self._current_backend().left_click() def right_click(self): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.RightClick()) - except Exception as exc: - self._log(f">>> [hardware_control] RightClick failed: {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.click(button="right") - return True - except Exception as exc: - self._log(f">>> [directinput] right click failed: {exc}") - return False + return self._current_backend().right_click() def left_down(self): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.LeftDown()) - except Exception as exc: - self._log(f">>> [hardware_control] LeftDown failed: {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.mouseDown(button="left") - return True - except Exception as exc: - self._log(f">>> [directinput] left mouseDown failed: {exc}") - return False + return self._current_backend().left_down() def left_up(self): - if self.uses_hardware_input(): - if self.is_available(): - try: - return bool(self._wyhkm.LeftUp()) - except Exception as exc: - self._log(f">>> [hardware_control] LeftUp failed: {exc}") - return False - if self._directinput_available(): - try: - pydirectinput.mouseUp(button="left") - return True - except Exception as exc: - self._log(f">>> [directinput] left mouseUp failed: {exc}") - return False + return self._current_backend().left_up() hw_ctrl = HardwareController() diff --git a/logistics_manager.py b/logistics_manager.py index dbd7764..89280ab 100644 --- a/logistics_manager.py +++ b/logistics_manager.py @@ -20,14 +20,20 @@ class LogisticsManager: self.route_file = route_file or VENDOR_FILE self.bag_full_hearthstone = False # 包满时用炉石回城而非走路修理 self.hearthstone_key = "b" # 炉石按键 - self.hearthstone_cast_sec = 10.0 # 炉石施法等待秒数 + self.hearthstone_cast_sec = 12.0 # 炉石施法等待秒数 self.enable_bag_full_mail = False + self.logistics_full_auto = False + self.bag_slot_threshold = 2 + self.durability_threshold = 0.2 self.mailbox_route_file = os.path.join("recorder", "mailbox.json") self.mailbox_interact_key = "8" self.mail_recipient_key = "" self.mail_send_key = "f8" self.mailbox_open_wait_sec = 2.0 self.mail_send_wait_sec = 60.0 + self.repair_target_key = "8" + self.repair_interact_key = "4" + self.repair_interact_wait_sec = 2.0 def _resolve_path(self, path): if not path: @@ -47,14 +53,60 @@ class LogisticsManager: return False time.sleep(min(0.1, end_at - time.time())) return True + + def _read_position(self, get_state): + if not callable(get_state): + return None + st = get_state() + if not st: + return None + try: + x = st.get("x") + y = st.get("y") + if x is None or y is None: + return None + return float(x), float(y) + except Exception: + return None + def _state_bag_full(self, state): + try: + return int(state.get("free_slots", 0) or 0) < int(self.bag_slot_threshold) + except Exception: + return False + + def _state_durability_low(self, state): + try: + threshold = float(self.durability_threshold) + if threshold > 1.0: + threshold = threshold / 100.0 + durability = state.get("durability", 1.0) + if durability is None: + return False + return float(durability) < threshold + except Exception: + return False + + def get_pending_tasks(self, state): + tasks = [] + bag_full = self._state_bag_full(state) + durability_low = self._state_durability_low(state) + + if bag_full and self.enable_bag_full_mail: + tasks.append("mail") + if durability_low: + tasks.append("repair") + if not tasks and bag_full and self.bag_full_hearthstone: + tasks.append("hearthstone_stop") + return tasks + def check_logistics(self, state): """ state['free_slots']: 剩余空格数量 state['durability']: 0.0 ~ 1.0 的耐久度 """ - # 触发阈值:空格少于 2 个,或耐久度低于 20% - if state['free_slots'] < 2 or state['durability'] < 0.2: + # 触发阈值:空格少于配置值,或耐久度低于配置值。 + if self.get_pending_tasks(state): if not self.is_returning: print(f">>> [后勤警告] 背包/耐久不足!触发回城程序。") self.is_returning = True @@ -67,11 +119,7 @@ class LogisticsManager: success = False for i in range(max_retries): - start_pos = None - if get_state: - st = get_state() - if st: - start_pos = (st.get('x'), st.get('y')) + start_pos = self._read_position(get_state) print(f">>> [后勤] 第 {i+1} 次尝试使用炉石(按键: {self.hearthstone_key})...") # 先按一下 S 确保停止移动,防止移动中按炉石失败 @@ -80,25 +128,34 @@ class LogisticsManager: hw_ctrl.press(self.hearthstone_key) # 等待施法过程 - print(f">>> [后勤] 正在等待施法 {self.hearthstone_cast_sec}s...") - time.sleep(self.hearthstone_cast_sec + 2.0) # 多等 2 秒保险 + print(f">>> [后勤] 正在等待炉石 {self.hearthstone_cast_sec}s...") + time.sleep(float(self.hearthstone_cast_sec)) - if get_state and start_pos and start_pos[0] is not None: - st_now = get_state() - if st_now: - end_pos = (st_now.get('x'), st_now.get('y')) + if get_state and start_pos is not None: + for _ in range(5): + end_pos = self._read_position(get_state) + if end_pos is None: + time.sleep(0.5) + continue dist = math.dist(start_pos, end_pos) # 如果坐标发生了明显跳变(大于 2.0),证明回城成功 if dist > 2.0: print(f">>> [后勤] 炉石回城成功!位置跳变距离: {dist:.2f}") success = True break - else: - print(f">>> [后勤] 炉石似乎失败(位置未变化),准备重试...") - else: + time.sleep(0.5) + if success: + break + print(f">>> [后勤] 炉石似乎失败(位置未变化),准备重试...") + elif get_state: + st_now = get_state() + if st_now is None: # 获取不到状态可能已经卡死或窗口关闭,默认成功以退出循环 success = True break + else: + success = True + break else: # 如果没法校验坐标,就只执行一次 success = True @@ -130,13 +187,13 @@ class LogisticsManager: self.is_returning = False def _do_vendor_interact(self): - """执行与修理商/背包的交互按键(8、4)。""" - hw_ctrl.press("8") + """执行与修理商/背包的交互按键。""" + hw_ctrl.press(self.repair_target_key) time.sleep(0.5) - hw_ctrl.press("4") - time.sleep(2) + hw_ctrl.press(self.repair_interact_key) + time.sleep(float(self.repair_interact_wait_sec)) - def run_bag_full_mail_flow(self, get_state, patrol, stop_check=None): + def run_bag_full_mail_flow(self, get_state, patrol, stop_check=None, use_hearthstone=True): """ 包满邮寄第一版流程: 炉石 -> 跑到邮箱路线终点 -> 交互打开邮箱 -> 按 MailboxCourier 宏键 -> 等待后停止。 @@ -152,11 +209,12 @@ class LogisticsManager: self.is_returning = False return False - ok = self.use_hearthstone_and_stop(get_state=get_state) - if not ok: - print(">>> [后勤-邮箱] 炉石失败,未继续跑邮箱。") - self.is_returning = False - return False + if use_hearthstone: + ok = self.use_hearthstone_and_stop(get_state=get_state) + if not ok: + print(">>> [后勤-邮箱] 炉石失败,未继续跑邮箱。") + self.is_returning = False + return False try: with open(route_file, "r", encoding="utf-8") as f: @@ -176,7 +234,13 @@ class LogisticsManager: if old_enable_mount is not None: patrol.enable_mount = False try: - ok = patrol.navigate_path(get_state, path, forward=True, arrival_threshold=VENDOR_ARRIVAL_THRESHOLD) + ok = patrol.navigate_path( + get_state, + path, + forward=True, + arrival_threshold=VENDOR_ARRIVAL_THRESHOLD, + snap_to_nearest=True, + ) finally: if old_enable_mount is not None: patrol.enable_mount = old_enable_mount @@ -213,6 +277,119 @@ class LogisticsManager: self.is_returning = False return True + def run_repair_flow(self, get_state, patrol, stop_check=None, use_hearthstone=True): + """ + 耐久低修理流程: + 炉石 -> 从修理路线最近点接入 -> 到达 NPC -> 按目标键 -> 按交互/修理键。 + """ + route_file = self._resolve_path(self.route_file) + if not route_file or not os.path.exists(route_file): + print(f">>> [后勤-修理] 修理路线不存在,已停止: {route_file}") + self.is_returning = False + return False + + if callable(stop_check) and stop_check(): + self.is_returning = False + return False + + if use_hearthstone: + ok = self.use_hearthstone_and_stop(get_state=get_state) + if not ok: + print(">>> [后勤-修理] 炉石失败,未继续跑修理路线。") + self.is_returning = False + return False + + try: + with open(route_file, "r", encoding="utf-8") as f: + path = json.load(f) + except Exception as exc: + print(f">>> [后勤-修理] 修理路线读取失败: {exc}") + self.is_returning = False + return False + + if not path: + print(f">>> [后勤-修理] 修理路线为空,已停止: {route_file}") + self.is_returning = False + return False + + print(f">>> [后勤-修理] 开始跑修理路线: {route_file}") + ok = patrol.navigate_path( + get_state, + path, + forward=True, + arrival_threshold=VENDOR_ARRIVAL_THRESHOLD, + snap_to_nearest=True, + ) + if not ok: + print(">>> [后勤-修理] 修理路线未完成,已停止。") + self.is_returning = False + return False + + if callable(stop_check) and stop_check(): + self.is_returning = False + return False + + patrol.stop_all() + print(f">>> [后勤-修理] 到达 NPC,按目标键: {self.repair_target_key}") + hw_ctrl.press(self.repair_target_key) + if not self._sleep_with_stop(0.5, stop_check=stop_check): + self.is_returning = False + return False + + print(f">>> [后勤-修理] 按修理交互键: {self.repair_interact_key}") + hw_ctrl.press(self.repair_interact_key) + if not self._sleep_with_stop(self.repair_interact_wait_sec, stop_check=stop_check): + self.is_returning = False + return False + + print(">>> [后勤-修理] 修理流程结束。") + self.is_returning = False + return True + + def run_pending_tasks(self, tasks, get_state, patrol, stop_check=None): + completed = [] + already_homed = False + for task in tasks: + if callable(stop_check) and stop_check(): + self.is_returning = False + return False, completed + + if task == "mail": + ok = self.run_bag_full_mail_flow( + get_state, + patrol, + stop_check=stop_check, + use_hearthstone=not already_homed, + ) + if not ok: + return False, completed + already_homed = True + completed.append(task) + continue + + if task == "repair": + ok = self.run_repair_flow( + get_state, + patrol, + stop_check=stop_check, + use_hearthstone=not already_homed, + ) + if not ok: + return False, completed + already_homed = True + completed.append(task) + continue + + if task == "hearthstone_stop": + ok = self.use_hearthstone_and_stop(get_state=get_state) + if not ok: + return False, completed + already_homed = True + completed.append(task) + + self.is_returning = False + return True, completed + def run_route1_round(self, get_state, patrol, route_file=None): """ 读取 route1.json 路径,先正向走完,执行交互(8、4),再反向走完,然后结束。 diff --git a/wow_multikey_gui.py b/wow_multikey_gui.py index 7f0e3fb..bf25a49 100644 --- a/wow_multikey_gui.py +++ b/wow_multikey_gui.py @@ -327,7 +327,9 @@ class GameLoopWorker(QThread): release_spirit_key=None, resurrect_key=None, enable_mouse_loot=True, - use_hardware_input=True, + use_hardware_input=None, + use_tianya_box=None, + use_ghost_box=None, turn_error_key=None, turn_error_hold_sec=None, distance_interact_pause_sec=None, @@ -374,7 +376,12 @@ class GameLoopWorker(QThread): self.release_spirit_key = release_spirit_key self.resurrect_key = resurrect_key self.enable_mouse_loot = enable_mouse_loot - self.use_hardware_input = bool(use_hardware_input) + if use_tianya_box is None and use_ghost_box is None: + self.use_tianya_box = True if use_hardware_input is None else bool(use_hardware_input) + self.use_ghost_box = False + else: + self.use_tianya_box = bool(use_tianya_box) + self.use_ghost_box = bool(use_ghost_box) self.turn_error_key = (turn_error_key or "s").strip().lower() or "s" try: self.turn_error_hold_sec = float(turn_error_hold_sec) @@ -393,10 +400,14 @@ class GameLoopWorker(QThread): self.log_signal.emit("❌ 无法导入 game_state 模块") return - hw_ctrl.configure(use_hardware_input=self.use_hardware_input) + hw_ctrl.configure(use_tianya_box=self.use_tianya_box, use_ghost_box=self.use_ghost_box) backend_text = { - "hardware": "硬件", - "hardware_unavailable": "硬件(不可用)", + "hardware": "天涯盒子", + "hardware_unavailable": "天涯盒子(不可用)", + "tianya": "天涯盒子", + "tianya_unavailable": "天涯盒子(不可用)", + "ghost": "幽灵盒子", + "ghost_unavailable": "幽灵盒子(不可用)", "directinput": "pydirectinput", "directinput_unavailable": "pydirectinput(不可用)", }.get(hw_ctrl.backend_label(), "未知") @@ -495,14 +506,19 @@ class GameLoopWorker(QThread): enable_mount=bool(layout.get("enable_mount", True)), ) self.logistics_manager = LogisticsManager(route_file=self.vendor_path) + self.logistics_manager.hearthstone_key = str(layout.get("hearthstone_key", "b") or "b") + self.logistics_manager.hearthstone_cast_sec = float(layout.get("hearthstone_wait_sec", 12.0)) + self.logistics_manager.repair_target_key = str(layout.get("repair_target_key", "8") or "8") + self.logistics_manager.repair_interact_key = str(layout.get("repair_interact_key", "4") or "4") self.log_signal.emit(f"⛑️ 回城修理:开始执行路径({os.path.basename(self.vendor_path)})") get_state = lambda: parse_game_state() if self.running else None - ok = self.logistics_manager.run_route1_round( + ok = self.logistics_manager.run_repair_flow( get_state, self.patrol_controller, - route_file=self.vendor_path, + stop_check=lambda: not self.running, + use_hearthstone=True, ) if ok: self.log_signal.emit("✅ 回城修理:路径完成") @@ -1069,6 +1085,18 @@ class WoWMultiKeyGUI(QMainWindow): params_layout.addWidget(basic_group) + # 硬件参数 + hardware_group = QGroupBox("硬件参数") + hardware_layout = QHBoxLayout(hardware_group) + self.gs_use_tianya_box = QCheckBox("使用天涯盒子") + self.gs_use_tianya_box.setChecked(True) + self.gs_use_ghost_box = QCheckBox("使用幽灵盒子") + self.gs_use_ghost_box.setChecked(False) + hardware_layout.addWidget(self.gs_use_tianya_box) + hardware_layout.addWidget(self.gs_use_ghost_box) + hardware_layout.addStretch() + params_layout.addWidget(hardware_group) + # 游戏参数 game_group = QGroupBox("游戏参数") game_grid = QGridLayout(game_group) @@ -1140,6 +1168,29 @@ class WoWMultiKeyGUI(QMainWindow): self.gs_mail_send_wait.setSingleStep(1.0) self.gs_mail_send_wait.setValue(60.0) self.gs_mail_send_wait.setSuffix(" 秒") + self.gs_logistics_full_auto = QCheckBox("后勤完成后继续自动巡逻") + self.gs_logistics_full_auto.setChecked(False) + self.gs_bag_slot_threshold = QSpinBox() + self.gs_bag_slot_threshold.setRange(0, 100) + self.gs_bag_slot_threshold.setValue(2) + self.gs_bag_slot_threshold.setSuffix(" 格") + self.gs_durability_threshold = QSpinBox() + self.gs_durability_threshold.setRange(1, 100) + self.gs_durability_threshold.setValue(20) + self.gs_durability_threshold.setSuffix(" %") + self.gs_hearthstone_wait = QDoubleSpinBox() + self.gs_hearthstone_wait.setRange(1.0, 60.0) + self.gs_hearthstone_wait.setSingleStep(0.5) + self.gs_hearthstone_wait.setValue(12.0) + self.gs_hearthstone_wait.setSuffix(" 秒") + self.gs_repair_target_key = QLineEdit() + self.gs_repair_target_key.setPlaceholderText("如 8") + self.gs_repair_target_key.setMaxLength(16) + self.gs_repair_target_key.setText("8") + self.gs_repair_interact_key = QLineEdit() + self.gs_repair_interact_key.setPlaceholderText("如 4") + self.gs_repair_interact_key.setMaxLength(16) + self.gs_repair_interact_key.setText("4") self.gs_enable_mount = QCheckBox("启用上马") self.gs_enable_mount.setChecked(True) self.gs_mount_key = QLineEdit() @@ -1166,8 +1217,6 @@ class WoWMultiKeyGUI(QMainWindow): self.gs_resurrect_key.setText("0") self.gs_enable_mouse_loot = QCheckBox("启用扫雷拾取") self.gs_enable_mouse_loot.setChecked(True) - self.gs_use_hardware_input = QCheckBox("启用硬件控制(关闭则用 pydirectinput)") - self.gs_use_hardware_input.setChecked(True) # 网格填充 game_grid.addWidget(QLabel("剥皮等待时间:"), 0, 0) @@ -1192,31 +1241,42 @@ class WoWMultiKeyGUI(QMainWindow): game_grid.addWidget(QLabel("炉石按键:"), 4, 0) game_grid.addWidget(self.gs_hearthstone_key, 4, 1) - game_grid.addWidget(QLabel("释放灵魂按键:"), 4, 2) - game_grid.addWidget(self.gs_release_spirit_key, 4, 3) + game_grid.addWidget(QLabel("炉石等待:"), 4, 2) + game_grid.addWidget(self.gs_hearthstone_wait, 4, 3) - game_grid.addWidget(QLabel("需转身按键:"), 5, 0) - game_grid.addWidget(self.turn_error_key_edit, 5, 1) - game_grid.addWidget(QLabel("需转身按住时长:"), 6, 0) - game_grid.addWidget(self.turn_error_hold_spin, 6, 1) + game_grid.addWidget(QLabel("释放灵魂按键:"), 5, 0) + game_grid.addWidget(self.gs_release_spirit_key, 5, 1) + game_grid.addWidget(QLabel("复活按键:"), 5, 2) + game_grid.addWidget(self.gs_resurrect_key, 5, 3) + + game_grid.addWidget(QLabel("需转身按键:"), 6, 0) + game_grid.addWidget(self.turn_error_key_edit, 6, 1) + game_grid.addWidget(QLabel("需转身按住时长:"), 6, 2) + game_grid.addWidget(self.turn_error_hold_spin, 6, 3) game_grid.addWidget(QLabel("距离远暂停技能时长:"), 7, 0) game_grid.addWidget(self.distance_interact_pause_spin, 7, 1) - game_grid.addWidget(self.gs_use_hardware_input, 8, 0, 1, 2) - game_grid.addWidget(QLabel("复活按键:"), 6, 2) - game_grid.addWidget(self.gs_resurrect_key, 6, 3) - game_grid.addWidget(self.gs_bag_full_hearthstone, 9, 0, 1, 2) - game_grid.addWidget(self.gs_enable_bag_full_mail, 9, 2, 1, 2) - game_grid.addWidget(QLabel("邮箱交互键:"), 10, 0) - game_grid.addWidget(self.gs_mailbox_interact_key, 10, 1) - game_grid.addWidget(QLabel("收信人按键:"), 10, 2) - game_grid.addWidget(self.gs_mail_recipient_key, 10, 3) - game_grid.addWidget(QLabel("邮寄宏按键:"), 11, 0) - game_grid.addWidget(self.gs_mail_send_key, 11, 1) - game_grid.addWidget(QLabel("邮箱打开等待:"), 11, 2) - game_grid.addWidget(self.gs_mailbox_open_wait, 11, 3) - game_grid.addWidget(QLabel("邮寄后等待:"), 12, 0) - game_grid.addWidget(self.gs_mail_send_wait, 12, 1) + game_grid.addWidget(self.gs_bag_full_hearthstone, 8, 0, 1, 2) + game_grid.addWidget(self.gs_enable_bag_full_mail, 8, 2, 1, 2) + game_grid.addWidget(QLabel("邮箱交互键:"), 9, 0) + game_grid.addWidget(self.gs_mailbox_interact_key, 9, 1) + game_grid.addWidget(QLabel("收信人按键:"), 9, 2) + game_grid.addWidget(self.gs_mail_recipient_key, 9, 3) + game_grid.addWidget(QLabel("邮寄宏按键:"), 10, 0) + game_grid.addWidget(self.gs_mail_send_key, 10, 1) + game_grid.addWidget(QLabel("邮箱打开等待:"), 10, 2) + game_grid.addWidget(self.gs_mailbox_open_wait, 10, 3) + game_grid.addWidget(QLabel("邮寄后等待:"), 11, 0) + game_grid.addWidget(self.gs_mail_send_wait, 11, 1) + game_grid.addWidget(self.gs_logistics_full_auto, 11, 2, 1, 2) + game_grid.addWidget(QLabel("包格触发阈值:"), 12, 0) + game_grid.addWidget(self.gs_bag_slot_threshold, 12, 1) + game_grid.addWidget(QLabel("耐久触发阈值:"), 12, 2) + game_grid.addWidget(self.gs_durability_threshold, 12, 3) + game_grid.addWidget(QLabel("修理目标键:"), 13, 0) + game_grid.addWidget(self.gs_repair_target_key, 13, 1) + game_grid.addWidget(QLabel("修理交互键:"), 13, 2) + game_grid.addWidget(self.gs_repair_interact_key, 13, 3) params_layout.addWidget(game_group) @@ -1263,6 +1323,15 @@ class WoWMultiKeyGUI(QMainWindow): self.gs_mail_send_key.setText(str(cfg.get('mail_send_key', 'f8') or 'f8')) self.gs_mailbox_open_wait.setValue(float(cfg.get('mailbox_open_wait_sec', 2.0))) self.gs_mail_send_wait.setValue(float(cfg.get('mail_send_wait_sec', 60.0))) + self.gs_logistics_full_auto.setChecked(bool(cfg.get('logistics_full_auto', False))) + self.gs_bag_slot_threshold.setValue(int(cfg.get('bag_slot_threshold', 2))) + durability_threshold = float(cfg.get('durability_threshold', 0.2)) + if durability_threshold <= 1.0: + durability_threshold *= 100.0 + self.gs_durability_threshold.setValue(int(round(durability_threshold))) + self.gs_hearthstone_wait.setValue(float(cfg.get('hearthstone_wait_sec', 12.0))) + self.gs_repair_target_key.setText(str(cfg.get('repair_target_key', '8') or '8')) + self.gs_repair_interact_key.setText(str(cfg.get('repair_interact_key', '4') or '4')) self.gs_mount_retry.setValue(float(cfg.get('mount_retry_after_sec', 2.0))) self.gs_release_spirit_key.setText(str(cfg.get('release_spirit_key', '9') or '9')) self.gs_resurrect_key.setText(str(cfg.get('resurrect_key', '0') or '0')) @@ -1279,7 +1348,15 @@ class WoWMultiKeyGUI(QMainWindow): self.turn_error_hold_spin.setValue(float(bot_cfg.get('turn_error_hold_sec', 0.8))) self.distance_interact_pause_spin.setValue(float(bot_cfg.get('distance_interact_pause_sec', 1.0))) self.gs_enable_mouse_loot.setChecked(bool(bot_cfg.get('enable_mouse_loot', True))) - self.gs_use_hardware_input.setChecked(bool(bot_cfg.get('use_hardware_input', True))) + has_box_flags = ('use_tianya_box' in bot_cfg) or ('use_ghost_box' in bot_cfg) + if has_box_flags: + use_tianya_box = bool(bot_cfg.get('use_tianya_box', False)) + use_ghost_box = bool(bot_cfg.get('use_ghost_box', False)) + else: + use_tianya_box = bool(bot_cfg.get('use_hardware_input', True)) + use_ghost_box = False + self.gs_use_tianya_box.setChecked(use_tianya_box) + self.gs_use_ghost_box.setChecked(use_ghost_box) except Exception: self.skinning_wait_spin.setValue(1.5) self.food_key_edit.setText('f1') @@ -1289,7 +1366,8 @@ class WoWMultiKeyGUI(QMainWindow): self.turn_error_hold_spin.setValue(0.8) self.distance_interact_pause_spin.setValue(1.0) self.gs_enable_mouse_loot.setChecked(True) - self.gs_use_hardware_input.setChecked(True) + self.gs_use_tianya_box.setChecked(True) + self.gs_use_ghost_box.setChecked(False) def _save_params_config(self): """保存「参数配置」界面到 game_state_config.json(多分辨率)并写入 wow_multikey_qt.json(bot 参数)""" @@ -1314,6 +1392,12 @@ class WoWMultiKeyGUI(QMainWindow): cfg['mail_send_key'] = self.gs_mail_send_key.text().strip() or 'f8' cfg['mailbox_open_wait_sec'] = float(self.gs_mailbox_open_wait.value()) cfg['mail_send_wait_sec'] = float(self.gs_mail_send_wait.value()) + cfg['logistics_full_auto'] = self.gs_logistics_full_auto.isChecked() + cfg['bag_slot_threshold'] = int(self.gs_bag_slot_threshold.value()) + cfg['durability_threshold'] = float(self.gs_durability_threshold.value()) / 100.0 + cfg['hearthstone_wait_sec'] = float(self.gs_hearthstone_wait.value()) + cfg['repair_target_key'] = self.gs_repair_target_key.text().strip() or '8' + cfg['repair_interact_key'] = self.gs_repair_interact_key.text().strip() or '4' cfg['release_spirit_key'] = (self.gs_release_spirit_key.text().strip() or '9') cfg['resurrect_key'] = (self.gs_resurrect_key.text().strip() or '0') path = save_layout_config(cfg) @@ -1328,10 +1412,14 @@ class WoWMultiKeyGUI(QMainWindow): self.config['bot']['turn_error_hold_sec'] = float(self.turn_error_hold_spin.value()) self.config['bot']['distance_interact_pause_sec'] = float(self.distance_interact_pause_spin.value()) self.config['bot']['enable_mouse_loot'] = self.gs_enable_mouse_loot.isChecked() - self.config['bot']['use_hardware_input'] = self.gs_use_hardware_input.isChecked() + use_tianya_box = self.gs_use_tianya_box.isChecked() + use_ghost_box = self.gs_use_ghost_box.isChecked() + self.config['bot']['use_tianya_box'] = use_tianya_box + self.config['bot']['use_ghost_box'] = use_ghost_box + self.config['bot']['use_hardware_input'] = use_tianya_box or use_ghost_box self._save_main_config() from hardware_control import hw_ctrl - hw_ctrl.configure(use_hardware_input=self.gs_use_hardware_input.isChecked()) + hw_ctrl.configure(use_tianya_box=use_tianya_box, use_ghost_box=use_ghost_box) self.log(f"✅ 参数配置已保存至 {path},并更新 bot 参数") QMessageBox.information(self, "保存成功", f"参数配置已保存至:\n{path}\n\nBot 参数已写入:\n{self.config_path}") @@ -1824,7 +1912,8 @@ class WoWMultiKeyGUI(QMainWindow): resurrect_key = '0' enable_mouse_loot = self.gs_enable_mouse_loot.isChecked() - use_hardware_input = self.gs_use_hardware_input.isChecked() + use_tianya_box = self.gs_use_tianya_box.isChecked() + use_ghost_box = self.gs_use_ghost_box.isChecked() self.game_worker = GameLoopWorker( mode, waypoints_path=waypoints_path, prepare_route_path=prepare_route_path, vendor_path=vendor_path, @@ -1847,7 +1936,8 @@ class WoWMultiKeyGUI(QMainWindow): release_spirit_key=release_spirit_key, resurrect_key=resurrect_key, enable_mouse_loot=enable_mouse_loot, - use_hardware_input=use_hardware_input, + use_tianya_box=use_tianya_box, + use_ghost_box=use_ghost_box, turn_error_key=turn_error_key, turn_error_hold_sec=turn_error_hold_sec, distance_interact_pause_sec=distance_interact_pause_sec, @@ -1880,7 +1970,13 @@ class WoWMultiKeyGUI(QMainWindow): min_dist = float(self.record_min_distance_spin.value()) except Exception: min_dist = 0.5 - self.game_worker = GameLoopWorker(mode='record', record_filename=name, record_min_distance=min_dist) + self.game_worker = GameLoopWorker( + mode='record', + record_filename=name, + record_min_distance=min_dist, + use_tianya_box=self.gs_use_tianya_box.isChecked(), + use_ghost_box=self.gs_use_ghost_box.isChecked(), + ) self.game_worker.state_signal.connect(self.record_state_label.setText) self.game_worker.log_signal.connect(self.log) self.game_worker.finished.connect(self._on_record_finished) diff --git a/wow_multikey_qt.json b/wow_multikey_qt.json index b043fdd..e161442 100644 --- a/wow_multikey_qt.json +++ b/wow_multikey_qt.json @@ -11,6 +11,12 @@ "food_key": "f1", "eat_hp_threshold": 30, "eat_max_wait_sec": 30.0, - "enable_mouse_loot": true + "enable_mouse_loot": true, + "use_tianya_box": true, + "use_ghost_box": false, + "use_hardware_input": true, + "turn_error_key": "4", + "turn_error_hold_sec": 0.5, + "distance_interact_pause_sec": 1.0 } -} \ No newline at end of file +}