import cv2
import numpy as np
import time
import win32gui
import win32con
from PIL import ImageGrab
import win32api
import os
import random
from image_processor import ImageProcessor
from mouse_controller import MouseController
from window_handler import WindowHandler
from logger import Logger
class GameWindowLostError(Exception):
"""游戏窗口丢失异常 - 用于终止任务执行"""
pass
class Assist:
def __init__(self, 设置页面, 日志输出=None,延时=None):
self.hwnd = None
self.日志输出 = 日志输出
self.img_processor = ImageProcessor( self.hwnd,self.日志输出)
self.mouse = MouseController( self.hwnd,self.日志输出)
self.延时 = 延时
self.window_handler = WindowHandler( self.日志输出,game_add =设置页面['gameaddress'])
self.设置页面 ={
'duanxinchonglian': 设置页面['duanxinchonglian'], #断线重连是否开启
'chongliantime': 设置页面['chongliantime'], #断线重连时间
'gameaddress': 设置页面['gameaddress'], #游戏地址
}
# 🔥 添加终止任务相关属性
self.终止标志 = False # 终止标志
self.stop_callback = None # 停止回调函数
self.main_thread_closer = None # 主线程关闭函数回调
#print("EnvironmentDetector实例已创建")
def 前往目的地(self,目的地):
imgpath ={}
if 目的地 == 'sj':
mudidi = '世界'
imgpath['cs'] = 'img/cs.bmp'
imgpath['cs2'] = 'img/cs2.bmp'
imgpath['cs3'] = 'img/cs3.bmp'
imgpath['cs4'] = 'img/cs4.bmp'
elif 目的地 == 'cs':
imgpath['sj'] = 'img/sj.bmp'
imgpath['sj2'] = 'img/sj2.bmp'
mudidi = '城市'
# 在 1113,583 1278,745中查找
pont = (6, 583, 1278, 745)
sj_location = self.img_processor.find_images(imgpath,region = pont)
if sj_location != None:
#print(f"找到{mudidi}按钮,正在点击")
self.日志输出.info(f"找到{mudidi}按钮,正在点击")
self.mouse.click_position(sj_location)
self.延时.delay()
self.移动开鼠标()
return True
return False
def 是否抵达目的地(self,目的地):
imgpath ={}
imgpaths ={
'cs': 'img/cs.bmp',
'cs2': 'img/cs2.bmp',
'cs3': 'img/cs3.bmp',
'cs4': 'img/cs4.bmp',
'sj': 'img/sj.bmp',
'sj2': 'img/sj2.bmp',
}
if 目的地 == 'sj':
mudidi = '世界'
imgpath['cs'] = 'img/cs.bmp'
imgpath['cs2'] = 'img/cs2.bmp'
imgpath['cs3'] = 'img/cs3.bmp'
imgpath['cs4'] = 'img/cs4.bmp'
elif 目的地 == 'cs':
imgpath['sj'] = 'img/sj.bmp'
imgpath['sj2'] = 'img/sj2.bmp'
mudidi = '城市'
# 在 1113,583 1278,745中查找
pont = (6, 583, 1278, 745)
# 查找城市 或者世界按钮
#判断管理箭头是否存在
cs_location = self.img_processor.find_images(imgpaths,0.85,region = pont)
if cs_location !=None :
if 目的地=='cs':
if cs_location['key']=='cs' or cs_location['key']=='cs2' or cs_location['key']=='cs3' or cs_location['key']=='cs4':
#print(f"已抵达{mudidi}")
self.日志输出.info(f"已抵达{mudidi}")
return True
elif 目的地=='sj':
if cs_location['key']=='sj' or cs_location['key']=='sj2':
#print(f"已抵达{mudidi}")
self.日志输出.info(f"已抵达{mudidi}")
return True
return False
def gotocs(self,addres):
"""
执行回到城市的操作 sj 世界 cs 城市
1.首先判断现在是在cs 还是世界 如果已经在则返回
2.如果没在
"""
for i in range(3):
if self.是否抵达目的地(addres):
self.日志输出.info(f"已抵达{'城市' if addres=='cs' else '世界'}")
return True
if self.前往目的地(addres):
return True
self.延时.delay()
self.清理对话框()
self.移动开鼠标
self.日志输出.info(f"回到{'城市' if addres=='cs' else '世界'}失败")
return False
def 世界或者城市(self):
"""判断 是在世界 还是城市"""
imgpaths ={
'cs': 'img/cs.bmp',
'cs2': 'img/cs2.bmp',
'cs3': 'img/cs3.bmp',
'cs4': 'img/cs4.bmp',
'sj': 'img/sj.bmp',
'sj2': 'img/sj2.bmp',
}
世界城市 = self.img_processor.find_images(imgpaths,threshold=0.75)
if not 世界城市:
return False
elif 世界城市['key'] == 'cs' or 世界城市['key'] == 'cs2' or 世界城市['key'] == 'cs3' or 世界城市['key'] == 'cs4':
return 'cs'
elif 世界城市['key'] == 'sj' or 世界城市['key'] == 'sj2':
return 'sj'
return None
def get_window_rect(self):
"""获取窗口位置和大小"""
rect = win32gui.GetWindowRect(self.hwnd)
return rect
def get_duilie_count(self):
"""获取队列数量 全部没有出征 返回6 否则返回对应数量"""
if not self.check_window_and_mouse():
self.日志输出.info("窗口不存在或不是当前窗口")
return False
if not self.清理对话框():
self.日志输出.info("清理对话框失败")
#首次查找gang是否存在 如果不存在 查找 dljt是否存在 如果不存在
dljt_location = self.img_processor.find_image('img/dljt.bmp',threshold=0.75)
if dljt_location == None:
#print("没有队伍出征还能派出很多")
self.日志输出.info("没有队伍出征")
return 8
#查找最大序列
max_dl = self.get_dl_count(type = 'zd')
if max_dl == None:
return None
#print(f"最大序列为: {max_dl}")
self.日志输出.info(f"最大序列为: {max_dl}")
dq_dl_count = self.get_dl_count(type ='dq')
#print(f"当前出征队列数量为: {dq_dl_count}")
self.日志输出.info(f"当前出征队列数量为: {dq_dl_count}")
if max_dl == None:
#print("获取最大序列失败")
self.日志输出.info("获取最大序列失败")
return None
if max_dl == 0:
#print("没有队列出征,可以尽情的派遣")
self.日志输出.info("没有队列出征,可以尽情的派遣")
return max_dl
if dq_dl_count == None:
#print("获取当前队列数量失败")
self.日志输出.info("获取当前队列数量失败")
return None
return max_dl - dq_dl_count
#获得最大序列
def get_dl_count(self,type='dq'):
"""序列数量 type=dq 为获取当前队列数量 type=max为获取当前最大序列数量
dq 为获取当前出征队列 zd为最大可出征的队列
"""
self.gotocs('cs')
region = self.img_processor.find_image('img/dljt.bmp',threshold=0.75)
if region == None:
return 0
newregin = None
if type =='zd':
newregin = (region['x']+region['width']+32, region['y'],region['x']+region['width']+42 , region['y']+15)
# print(f"get_dl_count中获取当前最大序列数量截图范围:{region}")
else:
newregin = (region['x']+region['width']+21, region['y'],region['x']+region['width']+30 , region['y']+15)
# print(f"get_dl_count获取当前队列数量:截图范围{newregin}")
dlimgs={
'dl1': 'img/dl1.bmp',
'dl11': 'img/dl11.bmp',
'dl2': 'img/dl2.bmp',
'dl21': 'img/dl21.bmp',
'dl3': 'img/dl3.bmp',
'dl31': 'img/dl31.bmp',
'dl32': 'img/dl32.bmp',
'dl4': 'img/dl4.bmp',
'dl41': 'img/dl41.bmp',
'dl5': 'img/dl5.bmp',
'dl51': 'img/dl51.bmp',
'dl6': 'img/dl6.bmp',
'dl7': 'img/dl7.bmp',
'dl62': 'img/dl62.bmp',
}
# window_image = self.img_processor.capture_window(newregin)
# # 保存截图到seedpic目录
# os.makedirs("seedpic", exist_ok=True)
# timestamp = time.strftime("%Y%m%d_%H%M%S")
# save_path = f"seedpic/find_images_{timestamp}.png"
# cv2.imwrite(save_path, window_image)
max_count = None
max_sequence = self.img_processor.find_images( dlimgs,threshold=0.75,region=newregin)
if type =='dq':
self.日志输出.info(f"获取当前序列")
if max_sequence:
if max_sequence['key']=='dl1' or max_sequence['key']=='dl11':
max_count = 1
elif max_sequence['key']=='dl2' or max_sequence['key']=='dl21':
max_count = 2
elif max_sequence['key']=='dl3' or max_sequence['key']=='dl31' or max_sequence['key']=='dl32':
max_count = 3
elif max_sequence['key']=='dl4' or max_sequence['key']=='dl41':
max_count = 4
elif max_sequence['key']=='dl5' or max_sequence['key']=='dl51':
max_count = 5
elif max_sequence['key']=='dl6' or max_sequence['key']=='dl62':
max_count = 6
elif max_sequence['key']=='dl7':
max_count = 7
if max_count == None:
#print("获取序列数量失败")
if type =='dq':
self.日志输出.info(f"当前没有出征队列")
else:
self.日志输出.info(f"队列没有识别到,怀疑未出证")
return None
# print(f"找到{type}序列数量为: {max_count}")
return max_count
else:
# print(f"未找到{type}序列序列")
#日志输出 如果 type = dq 输出当前 如果 type = zd 输出最大
if type =='dq':
self.日志输出.info(f"当前没有出征队列")
else:
self.日志输出.info(f"当前最大序列为{max_count}")
return None
def 移动开鼠标(self,操作='移动'):
"""操作 移动 或者 移动点击"""
窗口 = self.get_window_rect()
#print(f"窗口{窗口}")
#获取两数直接的随机整数
随机x = random.randint(100, 窗口[2]-窗口[0]-200)
随机y = random.randint(100, 窗口[3]-窗口[1]-200)
#print(f"鼠标移动到窗口的{随机x} {随机y}")
if 操作 =='移动':
self.mouse.move_to_xy({'x':随机x , 'y': 随机y})
elif 操作 =='移动点击':
# 移动鼠标到指定位置
win32api.SetCursorPos((随机x, 随机y))
# 模拟鼠标左键点击
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN, 随机x, 随机y, 0, 0)
#随机延时
self.延时.delay()
win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP, 随机x, 随机y, 0, 0)
#延时
self.延时.delay()
def 检测游戏是否掉线(self):
"""
检测游戏是否掉线并处理
流程:
1. 检测游戏窗口是否存在,如果不存在则启动游戏并调用 self.游戏重新登录()
2. 检测是否有顶号或掉线提示,如果都没有则返回 False
3. 如有提示,点击确认按钮
4. 如果是顶号,判断断线重连功能是否开启,如开启则启动游戏并调用 self.游戏重新登录()
5. 游戏启动成功后,获取窗口句柄并更新到相关组件
Returns:
bool: 检测到掉线或顶号并成功处理返回 True,否则返回 False
"""
try:
# 记录详细的连接检测日志
self.日志输出.info("开始检测游戏连接状态", show_in_ui=False)
# 1. 检测游戏窗口是否存在
hwnd = self.window_handler.is_window_exists()
if not hwnd:
self.日志输出.warning("游戏窗口不存在")
# 🔥 无论是否开启重连,都需要处理窗口丢失的情况
if self.设置页面['duanxinchonglian'] != True:
self.关闭线程()
# 如果开启了重连,执行重连逻辑
# 调用游戏重新登录函数
login_result = self.游戏重新登录("登录")
if login_result:
# 重新获取窗口句柄并更新
new_hwnd = self.window_handler.is_window_exists()
if new_hwnd:
self._update_window_handle(new_hwnd)
self.日志输出.info("游戏窗口重新启动成功")
return True
else:
self.日志输出.error("游戏窗口重新启动失败")
else:
# 重要:窗口存在时也需要更新句柄
self._update_window_handle(hwnd)
# 2. 检测顶号或掉线提示
img = {
'顶号': 'img/dinghao.bmp',
'掉线': 'img/diaoxian.bmp',
}
顶号 = self.img_processor.find_images(img, threshold=0.75)
if not 顶号:
# 没有检测到断线提示,游戏连接正常
return False
# 🔥 无论是否开启重连,都需要处理窗口丢失的情况
if self.设置页面['duanxinchonglian'] != True:
self.关闭线程()
# 3. 处理检测到的情况 - 增强日志记录
if 顶号['key'] == '顶号':
self.日志输出.warning("检测到账号被顶号,连接已断开")
# 4. 判断是否开启断线重连
elif 顶号['key'] == '掉线':
self.日志输出.warning("检测到网络掉线,连接已断开")
self.延时.delay()
# 5. 查找并点击确认按钮
确认图片 = {
'确认1': 'img/jgqueren.bmp',
'确认2': 'img/jgwjqueren.bmp',
}
确认返回 = self.img_processor.find_images(确认图片, threshold=0.75)
if 确认返回:
self.日志输出.info(f"点击断线确认按钮: {确认返回['key']}")
self.mouse.click_position(确认返回)
self.延时.delay()
self.延时.delay()
self.延时.delay()
# 6. 根据设置决定是否重连
if self.设置页面['duanxinchonglian']:
self.日志输出.info("断线重连功能已开启,开始执行重新登录流程")
login_result = self.游戏重新登录()
if login_result:
new_hwnd = self.window_handler.is_window_exists()
if new_hwnd:
self._update_window_handle(new_hwnd)
self.日志输出.info("断线重连成功,游戏已重新连接")
return True
else:
self.日志输出.error("断线重连失败,无法获取游戏窗口句柄")
else:
self.日志输出.error("断线重连失败,游戏启动不成功")
else:
self.日志输出.warning("断线重连功能未开启,需要手动重新连接游戏")
return True
except Exception as e:
self.日志输出.error(f"检测游戏连接状态时发生错误: {str(e)}")
return False
def 关闭线程(self):
"""真正关闭主线程 - 和F12效果一样"""
self.日志输出.warning("未开启重连,程序即将停止")
# 方法1: 通过返回特殊值通知调用者终止
self.终止标志 = True
# 方法2: 如果有主线程关闭函数,直接调用(优先级最高)
if hasattr(self, 'main_thread_closer') and callable(self.main_thread_closer):
try:
#self.日志输出.info("正在调用主线程关闭函数...")
self.main_thread_closer() # 直接调用主程序的关闭线程函数
#self.日志输出.info("主线程关闭函数已调用")
return # 主线程关闭函数会处理一切,不需要继续
except Exception as e:
self.日志输出.error(f"线程关闭失败: {e}")
# 方法3: 如果有业务逻辑停止回调,通知主程序停止
if hasattr(self, 'stop_callback') and callable(self.stop_callback):
try:
self.stop_callback()
self.日志输出.info("正在停止")
except Exception as e:
self.日志输出.error(f"停止失败: {e}")
# 方法4: 抛出特殊异常来强制终止
raise GameWindowLostError("未开启重连功能,任务终止")
def _update_window_handle(self, hwnd):
"""
更新窗口句柄到相关组件
Args:
hwnd: 窗口句柄
"""
try:
self.hwnd = hwnd
if hasattr(self, 'img_processor'):
self.img_processor.hwnd = hwnd
if hasattr(self, 'mouse'):
self.mouse.hwnd = hwnd
self.日志输出.debug(f"更新窗口句柄成功: {hwnd}")
except Exception as e:
self.日志输出.error(f"更新窗口句柄时发生错误: {str(e)}")
def 游戏重新登录(self,类型="等待登录"):
"""
类型 登录 2 等待登录
执行登录游戏
1.如果类型为等待登录 创建一个定时器 等待时间为 self.chongliantime + 0 或者(self.chongliantime *self.延时.random_factor)
2.调用 self.window_handler.启动游戏()
"""
try:
self.日志输出.info(f"开始执行游戏重新登录,类型: {类型}")
# 检查断线重连是否开启
# 处理等待登录模式
if 类型 == "等待登录":
# 计算等待时间
if hasattr(self, '设置页面') and 'chongliantime' in self.设置页面:
# 添加类型转换,确保wait_time为数字类型
wait_time = float(self.设置页面['chongliantime'])
# 如果有延时模块并且有随机因子,则应用随机因子
if hasattr(self, '延时') and hasattr(self.延时, 'random_factor'):
#随机 wait_time 到 wait_time * self.延时.random_factor
wait_time += random.uniform(0, wait_time * self.延时.random_factor)
wait_time = int(wait_time * 60)
self.日志输出.info(f"等待 {wait_time:.2f} 秒后开始重新登录")
# 使用时间戳循环检测替代time.sleep,定期提示剩余时间
start_time = time.time()
end_time = start_time + wait_time
last_notify_time = start_time
notify_interval = 10 # 每10秒提示一次剩余时间
while time.time() < end_time:
current_time = time.time()
remaining_time = end_time - current_time
# 定期提示剩余时间
if current_time - last_notify_time >= notify_interval:
self.日志输出.info(f"等待重新登录,还剩 {int(remaining_time)} 秒")
last_notify_time = current_time
# 短暂休眠,减少CPU占用但不阻塞界面
time.sleep(0.5)
self.日志输出.info("等待时间结束,开始重新登录")
else:
self.日志输出.warning("未找到等待时间配置,使用默认等待时间 20秒")
# 对于默认等待时间也使用循环检测方式
start_time = time.time()
end_time = start_time + 20
while time.time() < end_time:
remaining_time = end_time - time.time()
if int(remaining_time) % 10 == 0 and remaining_time > 0:
self.日志输出.info(f"使用默认等待时间,还剩 {int(remaining_time)} 秒")
time.sleep(0.5)
# 调用启动游戏方法
if hasattr(self, 'window_handler') and hasattr(self.window_handler, '启动游戏'):
self.日志输出.info("开始启动游戏")
result = self.window_handler.启动游戏()
if result:
self.日志输出.info("游戏启动成功")
else:
self.日志输出.error("游戏启动失败")
return result
except Exception as e:
self.日志输出.error(f"游戏重新登录过程中发生错误: {str(e)}")
return False
def 清理对话框(self):
"""
判断窗口是否被未知对话框遮挡
如果被遮挡 清理一下
"""
#首先判断 gljiantou.bmp 和 'sj': 'img/sj.bmp','sj2': 'img/sj2.bmp',
self.检测游戏是否掉线()
imgpaths ={
'cs': 'img/cs.bmp',
'cs2': 'img/cs2.bmp',
'cs3': 'img/cs3.bmp',
'cs4': 'img/cs4.bmp',
'sj': 'img/sj.bmp',
'sj2': 'img/sj2.bmp'
}
无遮挡图片={
'xing': 'img/zuobiaoxing.bmp',
'guanli': 'img/gljiantou.bmp',
}
pont = (6, 583, 1278, 745)
zuobia =(1,1,380,100)
for i in range(3):
无遮挡返回 =self.img_processor.find_images(无遮挡图片, threshold=0.85,region=zuobia)
对话框返回 =self.img_processor.find_images(imgpaths, threshold=0.85,region=pont)
if 无遮挡返回 and 对话框返回:
self.日志输出.info("窗口正常无遮挡")
return True
self.日志输出.info("关闭对话框")
# 使用win32con模块中的ESC虚拟键码
self.mouse.press_key(win32con.VK_ESCAPE)
self.延时.delay()
return False
def check_window_and_mouse(self):
"""检查窗口是否存在、是否为当前窗口,并处理鼠标位置"""
# 1. 判断窗口是否存在
if not win32gui.IsWindow(self.hwnd):
self.日志输出.info("窗口不存在")
return False
# 2. 判断是否为当前窗口,如果不是则设为当前
current_hwnd = win32gui.GetForegroundWindow()
if current_hwnd != self.hwnd:
self.日志输出.info("窗口不是当前窗口,正在设置为当前窗口")
# 尝试方法1: 发送ALT键事件以绕过焦点限制
win32api.keybd_event(18, 0, win32con.KEYEVENTF_EXTENDEDKEY, 0)
time.sleep(0.1)
win32api.keybd_event(18, 0, win32con.KEYEVENTF_EXTENDEDKEY | win32con.KEYEVENTF_KEYUP, 0)
time.sleep(0.1)
# 尝试方法2: 使用SetForegroundWindow设置为当前窗口
try:
result = win32gui.SetForegroundWindow(self.hwnd)
if result == 0:
self.日志输出.warning("SetForegroundWindow调用失败,尝试使用SetWindowPos")
# 尝试方法3: 设置窗口为最顶层
win32gui.SetWindowPos(self.hwnd, win32con.HWND_TOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
time.sleep(0.2)
# 再次尝试SetForegroundWindow
win32gui.SetForegroundWindow(self.hwnd)
# 取消最顶层设置
win32gui.SetWindowPos(self.hwnd, win32con.HWND_NOTOPMOST, 0, 0, 0, 0, win32con.SWP_NOMOVE | win32con.SWP_NOSIZE)
except Exception as e:
self.日志输出.error(f"设置窗口为当前窗口时出错: {str(e)}")
self.延时.delay()
# 3. 判断鼠标是否在窗口内,如果不在则调用移动开鼠标
window_rect = self.get_window_rect()
mouse_pos = win32api.GetCursorPos()
# 检查鼠标是否在窗口内
if not (window_rect[0] <= mouse_pos[0] <= window_rect[2] and window_rect[1] <= mouse_pos[1] <= window_rect[3]):
self.日志输出.info("鼠标不在窗口内,正在移动鼠标")
self.移动开鼠标()
return True