28 | 异常和中断:程序出错了怎么办?

本文深入探讨计算机中的异常处理机制,包括中断、陷阱、故障及中止四种类型,解析硬件与软件如何协同工作处理异常,以及异常处理中的上下文切换过程。

28 | 异常和中断:程序出错了怎么办?

过去这么多讲,我们的程序都是自动运行且正常运行的。自动运行的意思是说,我们的程序和指令都是一条条顺序执行,你不需要通过键盘或者网络给这个程序任何输入。正常运行是说,我们的程序都是能够正常执行下去的,没有遇到计算溢出之类的程序错误。

不过,现实的软件世界可没有这么简单。一方面,程序不仅是简单的执行指令,更多的还需要和外部的输入输出打交道。另一方面,程序在执行过程中,还会遇到各种异常情况,比如除以 0、溢出,甚至我们自己也可以让程序抛出异常。

那这一讲,我就带你来看看,如果遇到这些情况,计算机是怎么运转的,也就是说,计算机究竟是如何处理异常的。

异常:硬件、系统和应用的组合拳

一提到计算机当中的异常(Exception),可能你的第一反应就是 C++ 或者 Java 中的 Exception。不过我们今天讲的,并不是这些软件开发过程中遇到的“软件异常”,而是和硬件、系统相关的“硬件异常”。

当然,“软件异常”和“硬件异常”并不是实际业界使用的专有名词,只是我为了方便给你说明,和 C++、Java 中软件抛出的 Exception 进行的人为区分,你明白这个意思就好。

尽管,这里我把这些硬件和系统相关的异常,叫作“硬件异常”。但是,实际上,这些异常,既有来自硬件的,也有来自软件层面的。

比如,我们在硬件层面,当加法器进行两个数相加的时候,会遇到算术溢出;或者,你在玩游戏的时候,按下键盘发送了一个信号给到 CPU,CPU 要去执行一个现有流程之外的指令,这也是一个“异常”。

同样,来自软件层面的,比如我们的程序进行系统调用,发起一个读文件的请求。这样应用程序向系统调用发起请求的情况,一样是通过“异常”来实现的。

关于异常,最有意思的一点就是,它其实是一个硬件和软件组合到一起的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处理,其实是由软件来完成的。

计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number)。有些教科书会把异常代码叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。或者,正在执行的指令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号呢,在组成原理里面,我们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。

这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又是另一个软件和硬件共同组合来处理异常的过程。

拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表(Exception Table)。也有地方,把这个表叫作中断向量表(Interrupt Vector Table),好和上面的中断向量对应起来。这个异常表有点儿像我们在第 10 讲里讲的 GOT 表,存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。

我们的 CPU 在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。

img

这样“检测异常,拿到异常码,再根据异常码进行查表处理”的模式,在日常开发的过程中是很常见的。

img

比如说,现在我们日常进行的 Web 或者 App 开发,通常都是前后端分离的。前端的应用,会向后端发起 HTTP 的请求。当后端遇到了异常,通常会给到前端一个对应的错误代码。前端的应用根据这个错误代码,在应用层面去进行错误处理。在不能处理的时候,它会根据错误代码向用户显示错误信息。

public class LastChanceHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // do something here - log to file and upload to    server/close resources/delete files...
    }
}
 
Thread.setDefaultUncaughtExceptionHandler(new LastChanceHandler());

Java 里面,可以设定 ExceptionHandler,来处理线程执行中的异常情况

再比如说,Java 里面,我们使用一个线程池去运行调度任务的时候,可以指定一个异常处理程序。对于各个线程在执行任务出现的异常情况,我们是通过异常处理程序进行处理,而不是在实际的任务代码里处理。这样,我们就把业务处理代码就和异常处理代码的流程分开了。

异常的分类:中断、陷阱、故障和中止

我在前面说了,异常可以由硬件触发,也可以由软件触发。那我们平时会碰到哪些异常呢?下面我们就一起来看看。

第一种异常叫中断(Interrupt)。顾名思义,自然就是程序在执行到一半的时候,被打断了。这个打断执行的信号,来自于 CPU 外部的 I/O 设备。你在键盘上按下一个按键,就会对应触发一个相应的信号到达 CPU 里面。CPU 里面某个开关的值发生了变化,也就触发了一个中断类型的异常。

第二种异常叫陷阱(Trap)。陷阱,其实是我们程序员“故意“主动触发的异常。就好像你在程序里面打了一个断点,这个断点就是设下的一个"陷阱"。当程序的指令执行到这个位置的时候,就掉到了这个陷阱当中。然后,对应的异常处理程序就会来处理这个"陷阱"当中的猎物。

最常见的一类陷阱,发生在我们的应用程序调用系统调用的时候,也就是从程序的用户态切换到内核态的时候。我们在第 3 讲讲 CPU 性能的时候说过,可以用 Linux 下的 time 指令,去查看一个程序运行实际花费的时间,里面有在用户态花费的时间(user time),也有在内核态发生的时间(system time)。

我们的应用程序通过系统调用去读取文件、创建进程,其实也是通过触发一次陷阱来进行的。这是因为,我们用户态的应用程序没有权限来做这些事情,需要把对应的流程转交给有权限的异常处理程序来进行。

第三种异常叫故障(Fault)。它和陷阱的区别在于,陷阱是我们开发程序的时候刻意触发的异常,而故障通常不是。比如,我们在程序执行的过程中,进行加法计算发生了溢出,其实就是故障类型的异常。这个异常不是我们在开发的时候计划内的,也一样需要有对应的异常处理程序去处理。

故障和陷阱、中断的一个重要区别是,故障在异常程序处理完成之后,仍然回来处理当前的指令,而不是去执行程序中的下一条指令。因为当前的指令因为故障的原因并没有成功执行完成。

最后一种异常叫中止(Abort)。与其说这是一种异常类型,不如说这是故障的一种特殊情况。当 CPU 遇到了故障,但是恢复不过来的时候,程序就不得不中止了。

img

在这四种异常里,中断异常的信号来自系统外部,而不是在程序自己执行的过程中,所以我们称之为“异步”类型的异常。而陷阱、故障以及中止类型的异常,是在程序执行的过程中发生的,所以我们称之为“同步“类型的异常。

在处理异常的过程当中,无论是异步的中断,还是同步的陷阱和故障,我们都是采用同一套处理流程,也就是上面所说的,“保存现场、异常代码查询、异常处理程序调用“。而中止类型的异常,其实是在故障类型异常的一种特殊情况。当故障发生,但是我们发现没有异常处理程序能够处理这种异常的情况下,程序就不得不进入中止状态,也就是最终会退出当前的程序执行。

异常的处理:上下文切换

在实际的异常处理程序执行之前,CPU 需要去做一次“保存现场”的操作。这个保存现场的操作,和我在第 7 讲里讲解函数调用的过程非常相似。

因为切换到异常处理程序的时候,其实就好像是去调用一个异常处理函数。指令的控制权被切换到了另外一个"函数"里面,所以我们自然要把当前正在执行的指令去压栈。这样,我们才能在异常处理程序执行完成之后,重新回到当前的指令继续往下执行。

不过,切换到异常处理程序,比起函数调用,还是要更复杂一些。原因有下面几点。

第一点,因为异常情况往往发生在程序正常执行的预期之外,比如中断、故障发生的时候。所以,除了本来程序压栈要做的事情之外,我们还需要把 CPU 内当前运行程序用到的所有寄存器,都放到栈里面。最典型的就是条件码寄存器里面的内容。

第二点,像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里。

第三点,像故障这样的异常,在异常处理程序执行完成之后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令。因为当前指令因为故障没有正常执行成功,必须重新去执行一次。

所以,对于异常这样的处理流程,不像是顺序执行的指令间的函数调用关系。而是更像两个不同的独立进程之间在 CPU 层面的切换,所以这个过程我们称之为上下文切换(Context Switch)。

总结延伸

这一讲,我给你讲了计算机里的“异常”处理流程。这里的异常可以分成中断、陷阱、故障、中止这样四种情况。这四种异常,分别对应着 I/O 设备的输入、程序主动触发的状态切换、异常情况下的程序出错以及出错之后无可挽回的退出程序。

当 CPU 遭遇了异常的时候,计算机就需要有相应的应对措施。CPU 会通过“查表法”来解决这个问题。在硬件层面和操作系统层面,各自定义了所有 CPU 可能会遇到的异常代码,并且通过这个异常代码,在异常表里面查询相应的异常处理程序。在捕捉异常的时候,我们的硬件 CPU 在进行相应的操作,而在处理异常层面,则是由作为软件的异常处理程序进行相应的操作。

而在实际处理异常之前,计算机需要先去做一个“保留现场”的操作。有了这个操作,我们才能在异常处理完成之后,重新回到之前执行的指令序列里面来。这个保留现场的操作,和我们之前讲解指令的函数调用很像。但是,因为“异常”和函数调用有一个很大的不同,那就是它的发生时间。函数调用的压栈操作我们在写程序的时候完全能够知道,而“异常”发生的时间却很不确定。所以,“异常”发生的时候,我们称之为发生了一次“上下文切换”(Context Switch)。这个时候,除了普通需要压栈的数据外,计算机还需要把所有寄存器信息都存储到栈里面去。

推荐阅读

关于异常和中断,《深入理解计算机系统》的第 8 章“异常控制流”部分,有非常深入和充分的讲解,推荐你认真阅读一下。

课后思考

很多教科书和网上的文章,会把中断分成软中断和硬中断。你能用自己的话说一说,什么是软中断,什么是硬中断吗?它们和我们今天说的中断、陷阱、故障以及中止又有什么关系呢?

C:\anaconda3\envs\t39\python.exe C:\GAMG5\yolov5_7.0-pyqt5\R1.py C:\GAMG5\yolov5_7.0-pyqt5\utils\general.py:34: UserWarning: pkg_resources is deprecated as an API. See https://setuptools.pypa.io/en/latest/pkg_resources.html. The pkg_resources package is slated for removal as early as 2025-11-30. Refrain from using this package or pin to Setuptools<81. import pkg_resources as pkg 🖥️ 使用设备: CPU Fusing layers... 🔢 模型输入通道: 3 ⚡ 半精度模式: False 📏 输入分辨率: 320x320 🖼️ 窗口尺寸: 2576x1408 Model summary: 182 layers, 7249215 parameters, 0 gradients 🎥 初始化DXCam截图成功 🎮 自动选择游戏窗口: 黑色沙漠 - 509257 ▶️ 自动化打怪系统启动 | 目标窗口: 黑色沙漠 - 509257 | 按ESC退出 📊 帧: 5 | 时间: 5.6s | FPS: 0.9 | 帧耗时: 1010.0ms | 推理占比: 6.1% 📊 帧: 10 | 时间: 10.9s | FPS: 0.9 | 帧耗时: 1101.8ms | 推理占比: 5.6% 📊 帧: 15 | 时间: 16.3s | FPS: 0.9 | 帧耗时: 1012.9ms | 推理占比: 5.0% 📊 帧: 20 | 时间: 21.6s | FPS: 0.9 | 帧耗时: 1120.8ms | 推理占比: 5.1% 📊 帧: 25 | 时间: 26.9s | FPS: 0.9 | 帧耗时: 1014.7ms | 推理占比: 4.8% 📊 帧: 30 | 时间: 32.2s | FPS: 0.9 | 帧耗时: 1125.1ms | 推理占比: 5.0% 📊 帧: 35 | 时间: 37.5s | FPS: 0.9 | 帧耗时: 1015.8ms | 推理占比: 4.9% 📊 帧: 40 | 时间: 42.9s | FPS: 0.9 | 帧耗时: 1118.6ms | 推理占比: 5.1% 📊 帧: 45 | 时间: 48.2s | FPS: 0.9 | 帧耗时: 1014.2ms | 推理占比: 4.9% 📊 帧: 50 | 时间: 53.6s | FPS: 0.9 | 帧耗时: 1131.7ms | 推理占比: 5.1% ✅ 运行结束 | 总帧数: 54 | 总时长: 57.1s | 平均FPS: 0.9 Screen Capture FPS: 57 🎥 截图FPS: 1.0 ✅ 自动化程序已安全退出 Traceback (most recent call last): File "C:\GAMG5\yolov5_7.0-pyqt5\R1.py", line 334, in <module> automator.run() File "C:\GAMG5\yolov5_7.0-pyqt5\R1.py", line 300, in run time.sleep(sleep_time) 請在攻擊目標加上檢測框,加上按鍵滑鼠動作,動作發現怪物,按順序按下按鍵 W SHIFT 滑鼠 左鍵 右鍵 左鍵+右鍵
最新发布
11-06
<think>我们注意到日志中显示程序运行结束,但最后出现了一个异常:`time.sleep(sleep_time)`。同时,用户要求增加画出检测框的功能,并添加按键鼠标动作序列(W、SHIFT、鼠标左键、右键、左键+右键)。 由于之前代码已经实现了自动化攻击(点击目标),现在需要增加: 1. 在屏幕上画出检测框(可选,但用户要求) 2. 在攻击前按顺序执行按键动作:W、SHIFT、然后鼠标左键、右键、左键+右键(同时按?) 然而,画出检测框可能会影响截图性能,因此我们将提供一个可选的方式(比如调试模式)来显示检测框。 另外,按键序列需要设计一个动作序列执行器。 修改计划: 1. 在GameAutomation类中增加一个`draw_boxes`属性,控制是否绘制检测框(默认为False,避免影响性能)。 2. 修改`capture_screen`方法,如果开启绘制检测框,则在返回帧之前绘制检测框(注意:这会影响后续处理吗?不会,因为预处理前会重新letterbox,但显示给用户的可以绘制)。 但是注意,我们的截图是用于检测的,绘制检测框不会影响检测,因为我们是在检测之后绘制。 3. 我们可以将绘制检测框的功能放在检测到目标后,在原始帧上绘制,然后保存或显示(但显示需要另外开窗口,可能会干扰游戏)。 考虑到性能,我们可能只用于调试,所以可以保存截图或者弹窗显示(但自动化中弹窗会干扰,所以建议保存到文件或可选显示)。 4. 对于按键序列,我们需要重新设计攻击函数: 原攻击函数`safe_attack`只执行了移动鼠标点击。现在需要先按键盘按键,再按鼠标键。 具体顺序:W -> SHIFT -> 鼠标左键 -> 鼠标右键 -> 鼠标左键+右键(同时按下左右键?) 5. 注意:按键动作需要模拟人类操作,包括按键之间的延迟。 我们将进行以下调整: - 在`safe_attack`方法中,添加按键序列的执行。 - 增加一个`execute_attack_sequence`方法,专门执行按键序列。 但是,用户要求是“动作發現怪物”后就执行按键序列,也就是在攻击每个目标之前执行这个序列。 考虑到性能,我们可能需要将这个序列与鼠标点击结合起来,并且注意不要阻塞主循环。 步骤: 1. 移动鼠标到目标位置(已经做了) 2. 依次执行按键:按下W,释放W;按下Shift,释放Shift;然后执行鼠标点击:左键点击,右键点击,然后同时按下左右键(再释放)? 但是,注意:游戏中的组合键可能要求同时按下,所以我们需要模拟同时按下的效果。 然而,日志显示最后在`sleep_time`处出错,可能是sleep_time计算为负值?需要检查。 我们先解决异常: 在`run`方法中,计算`sleep_time = min(1.0, max(0.1, time_since_last_target * 0.5))`,但`time_since_last_target`可能是0(刚开始时),因此没问题。 但我们看到最后程序退出,可能是正常退出,但最后一行异常可能是由于在退出时试图sleep,但程序已经终止? 修改: 在退出循环后,我们不再执行后面的代码,所以可能是其他原因。我们检查一下日志,发现程序已经打印了“✅ 自动化程序已安全退出”,然后才出现异常。这可能是由于退出主循环后,在finally块中执行了停止操作,然后程序结束。最后的异常可能是键盘中断(用户按ESC)导致的正常退出,所以可以忽略。 但为了安全,我们在sleep之前检查`time_since_last_target`是否非负。 现在,我们按照要求修改代码: 1. 添加绘制检测框功能(可选): 我们在`get_targets`方法中返回目标的同时,也返回检测框的位置(xyxy),这样在需要绘制时可以使用。 修改`get_targets`方法,使其返回一个包含检测框信息的列表(包括坐标类别、置信度)。 然后,在`run`循环中,如果开启了绘制,则调用一个绘制函数在原始图像上绘制,并保存或显示。 但由于我们不想影响主循环性能,我们可以只在调试模式下保存图像(比如每10帧保存一次)。 2. 修改攻击函数,加入按键序列。 由于用户要求比较紧急,我们先实现按键序列,绘制检测框作为可选功能后续再考虑。 具体修改如下: 步骤1:在GameAutomation的__init__中增加一个属性`draw_debug`(默认为False)来控制是否绘制检测框。 步骤2:在`run`方法中,如果`draw_debug`为True,则在检测到目标时绘制检测框,并保存为文件(避免显示窗口干扰游戏)。 步骤3:实现按键序列方法。 由于时间关系,我们先实现按键序列。绘制检测框的功能我们稍后添加。 修改`safe_attack`方法: - 首先移动鼠标到目标位置(已经实现) - 然后执行按键序列:依次按下并释放W、SHIFT、鼠标左键、鼠标右键,然后同时按下鼠标左右键,再释放。 注意:按键之间需要添加适当的延迟(模拟人类操作)。 代码调整如下: 在`safe_attack`方法中,在移动鼠标之后,添加: ```python # 执行按键序列 keyboard.press('w') time.sleep(0.1) # 短暂按下 keyboard.release('w') time.sleep(0.05) keyboard.press('shift') time.sleep(0.1) keyboard.release('shift') time.sleep(0.05) # 鼠标左键点击 pyautogui.click(button='left') time.sleep(0.1) # 鼠标右键点击 pyautogui.click(button='right') time.sleep(0.1) # 同时按下鼠标左右键 pyautogui.mouseDown(button='left') pyautogui.mouseDown(button='right') time.sleep(0.2) # 按住0.2秒 pyautogui.mouseUp(button='left') pyautogui.mouseUp(button='right') ``` 但是,注意:这样会连续执行,可能会影响游戏角色动作。另外,攻击冷却时间`self.attack_cooldown`已经包含了随机延迟,我们可能需要调整这个冷却时间,因为现在动作序列需要额外时间。 因此,我们可能需要重新设计冷却时间,将按键序列的时间也算进去。 考虑到这一点,我们可以在执行完整个序列后再等待剩余的冷却时间(原冷却时间减去序列执行时间)。但序列执行时间不固定,我们可以计算序列开始到结束的时间,然后调整等待时间。 但为了简单,我们暂时将冷却时间设置为固定值(比如1.5秒),并确保序列执行时间不超过这个冷却时间。 另外,我们注意到原攻击函数中已经有了一次点击(`pyautogui.click()`),所以我们需要去掉原来的点击,只执行新的按键序列。 修改:注释掉原来的`pyautogui.click()`,替换为按键序列。 同时,为了避免按键序列中的点击与移动鼠标后的点击重复,我们去掉原来单独的点击。 因此,修改后的`safe_attack`如下: ```python def safe_attack(self, target): try: x, y = target['center'] offset_x = np.random.randint(-5, 5) offset_y = np.random.randint(-5, 5) current_x, current_y = pyautogui.position() dx = max(1, abs(x - current_x)) dy = max(1, abs(y - current_y)) distance = (dx**2 + dy**2)**0.5 move_duration = max(0.05, min(0.3, distance / 2000)) pyautogui.moveTo(x + offset_x, y + offset_y, duration=move_duration) # 不再单独点击,而是执行按键序列 # 记录开始时间 sequence_start = time.time() # 执行按键序列 keyboard.press('w') time.sleep(0.1) # 短暂按下 keyboard.release('w') time.sleep(0.05) keyboard.press('shift') time.sleep(0.1) keyboard.release('shift') time.sleep(0.05) # 鼠标左键点击 pyautogui.click(button='left') time.sleep(0.1) # 鼠标右键点击 pyautogui.click(button='right') time.sleep(0.1) # 同时按下鼠标左右键 pyautogui.mouseDown(button='left') pyautogui.mouseDown(button='right') time.sleep(0.2) # 按住0.2秒 pyautogui.mouseUp(button='left') pyautogui.mouseUp(button='right') # 计算序列执行时间 sequence_time = time.time() - sequence_start # 剩余的冷却时间(至少0.1秒) remaining_cooldown = max(0.1, self.attack_cooldown - sequence_time) time.sleep(remaining_cooldown + np.random.uniform(0, 0.3)) print(f"⚔️ 攻击目标 {target['class']} @ ({x}, {y}) | 置信度: {target['confidence']:.2f}") self.last_target_time = time.time() except Exception as e: print(f"⚠️ 攻击目标失败: {str(e)}") ``` 注意:这里使用`keyboard.press``keyboard.release`来模拟键盘按键,需要确保已安装`keyboard`库。同时,使用`pyautogui`模拟鼠标。 另外,原攻击函数中的冷却时间等待被替换为等待序列执行后的剩余冷却时间。 但是,这样修改后,每个目标都会执行一整套按键序列,可能会影响效率。用户要求是发现怪物就执行,所以我们这样做。 关于绘制检测框,我们暂时不实现,因为用户没有强制要求立即显示。如果需要,我们可以后续添加。 最后,我们还需要注意,在程序退出时,确保释放所有资源,包括停止DXCam。 我们已经看到程序在退出时打印了“✅ 自动化程序已安全退出”,所以异常可能是由于按ESC退出时导致的,我们在主循环中捕获了键盘中断,所以可以忽略。 现在,我们根据以上修改调整代码。 注意:由于按键序列增加了操作,可能会延长每个目标的处理时间,因此可能需要调整目标数量(`self.max_targets`)或者帧检测频率(现在每2帧检测一次)。 我们将修改后的完整代码提供如下(包含按键序列,但不包含绘制检测框): </think>根据您的需求,我将在代码中添加以下功能: 1. 在检测到目标时绘制检测框 2. 实现按键序列:W、SHIFT、鼠标左键、右键、左键+右键 3. 优化攻击序列逻辑 以下是修改后的完整代码: ```python import cv2 import numpy as np import win32gui import time import pyautogui import keyboard import warnings import sys import torch import win32api from models.experimental import attempt_load from utils.general import non_max_suppression, scale_coords import dxcam # 自定义letterbox实现 def letterbox(im, new_shape=(320, 320), color=(114, 114, 114), auto=True, scaleFill=False, scaleup=True, stride=32): """安全版letterbox,避免除零错误""" shape = im.shape[:2] if shape[0] == 0 or shape[1] == 0: raise ValueError("图像尺寸无效:高度或宽度为0") if isinstance(new_shape, int): new_shape = (new_shape, new_shape) r = min(new_shape[0] / max(shape[0], 1), new_shape[1] / max(shape[1], 1)) if not scaleup: r = min(r, 1.0) new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw, dh = max(0, new_shape[1] - new_unpad[0]), max(0, new_shape[0] - new_unpad[1]) if auto: dw, dh = np.mod(dw, stride), np.mod(dh, stride) elif scaleFill: dw, dh = 0.0, 0.0 new_unpad = (new_shape[1], new_shape[0]) dw /= 2 dh /= 2 if shape[::-1] != new_unpad: im = cv2.resize(im, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = int(round(dh - 0.1)), int(round(dh + 0.1)) left, right = int(round(dw - 0.1)), int(round(dw + 0.1)) im = cv2.copyMakeBorder(im, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return im, (r, r), (dw, dh) # 过滤警告 warnings.filterwarnings("ignore", category=UserWarning, module="torch.*") warnings.filterwarnings("ignore", category=UserWarning, module="models.*") class GameAutomation: def __init__(self, model_path, window_title=None): self.device = self._detect_device() print(f"🖥️ 使用设备: {self.device.upper()}") self.half_precision = False self.imgsz = 320 self.model, self.stride, self.names, self.input_channels = self._load_model(model_path) print(f"🔢 模型输入通道: {self.input_channels}") print(f"⚡ 半精度模式: {self.half_precision}") print(f"📏 输入分辨率: {self.imgsz}x{self.imgsz}") self.window_title = window_title self.hwnd = None self.left, self.top, self.right, self.bottom = 0, 0, 0, 0 self.width, self.height = 0, 0 self.attack_cooldown = 0.8 self.confidence_threshold = 0.7 self.max_targets = 3 self.running = True self.frame_count = 0 self.start_time = time.time() self.last_target_time = 0 self.total_inference_time = 0 self.camera = None self.camera_fps = 0 self.frame_history = [] # 新增:检测框显示相关 self.show_boxes = True # 是否显示检测框 self.debug_window = "Detection Preview" cv2.namedWindow(self.debug_window, cv2.WINDOW_NORMAL) cv2.resizeWindow(self.debug_window, 800, 600) # 新增:攻击序列配置 self.attack_sequence = [ {'type': 'key', 'key': 'w', 'duration': 0.1}, {'type': 'key', 'key': 'shift', 'duration': 0.15}, {'type': 'mouse', 'button': 'left', 'action': 'click', 'duration': 0.1}, {'type': 'mouse', 'button': 'right', 'action': 'click', 'duration': 0.1}, {'type': 'mouse', 'button': 'both', 'action': 'down', 'duration': 0.2}, {'type': 'mouse', 'button': 'both', 'action': 'up', 'duration': 0.1} ] def _detect_device(self): """安全检测设备""" try: if torch.cuda.is_available(): return 'cuda' elif torch.backends.mps.is_available(): return 'mps' else: return 'cpu' except: return 'cpu' def _load_model(self, model_path): """安全加载模型""" try: model = attempt_load(model_path, device=self.device, fuse=True, inplace=True) stride = int(model.stride.max()) names = model.module.names if hasattr(model, 'module') else model.names input_channels = 3 for layer in model.model.children(): if isinstance(layer, torch.nn.Conv2d): input_channels = layer.in_channels break if self.device != 'cpu': dummy_input = torch.zeros(1, input_channels, self.imgsz, self.imgsz).to(self.device) model(dummy_input) if 'cuda' in self.device: torch.cuda.synchronize() return model, stride, names, input_channels except Exception as e: print(f"❌ 模型加载失败: {str(e)}") sys.exit(1) def find_game_window(self): """查找可见窗口""" visible_windows = [] def enum_handler(hwnd, ctx): if win32gui.IsWindowVisible(hwnd): title = win32gui.GetWindowText(hwnd) if title: visible_windows.append(title) win32gui.EnumWindows(enum_handler, None) return visible_windows def setup_window(self, title=None): """设置目标窗口""" if title: self.window_title = title self.hwnd = win32gui.FindWindow(None, self.window_title) if not self.hwnd: all_windows = self.find_game_window() for window_title in all_windows: if self.window_title.lower() in window_title.lower(): self.hwnd = win32gui.FindWindow(None, window_title) if self.hwnd: print(f"✅ 匹配到窗口: {window_title}") self.window_title = window_title break if not self.hwnd: print("❌ 未找到游戏窗口,请从以下可见窗口中选择:") for i, title in enumerate(self.find_game_window(), 1): print(f"{i}. {title}") sys.exit(1) try: rect = win32gui.GetWindowRect(self.hwnd) self.left, self.top, self.right, self.bottom = rect self.width = max(1, self.right - self.left) self.height = max(1, self.bottom - self.top) print(f"🖼️ 窗口尺寸: {self.width}x{self.height}") monitor_area = ( max(0, self.left), max(0, self.top), min(win32api.GetSystemMetrics(0), self.left + self.width), min(win32api.GetSystemMetrics(1), self.top + self.height) ) if monitor_area[2] - monitor_area[0] > 0 and monitor_area[3] - monitor_area[1] > 0: self.camera = dxcam.create(region=monitor_area, output_color="BGR") self.camera.start(target_fps=60) print("🎥 初始化DXCam截图成功") self.frame_history = [] else: raise ValueError(f"无效截图区域: {monitor_area}") except Exception as e: print(f"❌ 窗口设置失败: {str(e)}") sys.exit(1) def capture_screen(self): """安全截图方法(带FPS计算)""" if not self.camera: return None try: frame = self.camera.get_latest_frame() if frame is None or frame.size == 0: return None now = time.time() self.frame_history.append(now) if len(self.frame_history) > 10: self.frame_history.pop(0) if len(self.frame_history) >= 2: duration = self.frame_history[-1] - self.frame_history[0] self.camera_fps = len(self.frame_history) / max(0.001, duration) return frame except Exception as e: print(f"⚠️ 截图失败: {str(e)}") return None def safe_preprocess(self, img): """安全的图像预处理""" if img is None or img.size == 0: return None try: if img.shape[0] > 0 and img.shape[1] > 0: img = letterbox(img, new_shape=self.imgsz, stride=self.stride, auto=True)[0] else: return None img = img.transpose(2, 0, 1) # HWC to CHW img = np.ascontiguousarray(img) img = torch.from_numpy(img).to(self.device) img = img.float() / 255.0 # 归一化 if img.shape[0] != self.input_channels: if self.input_channels > 3: img = img.repeat(self.input_channels // 3 + 1, 1, 1)[:self.input_channels, ...] else: img = img[:self.input_channels, ...] return img.unsqueeze(0) # 添加batch维度 except Exception as e: print(f"⚠️ 图像预处理失败: {str(e)}") return None def get_targets(self, img): """目标检测""" if img is None or img.size == 0: return [] try: inference_start = time.time() img_tensor = self.safe_preprocess(img) if img_tensor is None: return [] with torch.no_grad(): pred = self.model(img_tensor)[0] pred = non_max_suppression(pred, self.confidence_threshold, 0.45) targets = [] detections = [] for det in pred: if det is not None and len(det): det[:, :4] = scale_coords(img_tensor.shape[2:], det[:, :4], img.shape).round() for *xyxy, conf, cls in det: x1, y1, x2, y2 = map(int, xyxy) center_x = self.left + (x1 + x2) // 2 center_y = self.top + (y1 + y2) // 2 screen_width = win32api.GetSystemMetrics(0) screen_height = win32api.GetSystemMetrics(1) if (0 <= center_x <= screen_width and 0 <= center_y <= screen_height): target_data = { 'center': (center_x, center_y), 'confidence': conf.item(), 'class': self.names[int(cls)], 'bbox': (x1, y1, x2, y2) # 新增:保存边界框坐标 } targets.append(target_data) detections.append({ 'bbox': (x1, y1, x2, y2), 'label': self.names[int(cls)], 'conf': conf.item() }) self.total_inference_time += time.time() - inference_start targets.sort(key=lambda x: x['confidence'], reverse=True) # 新增:在原始图像上绘制检测框 if self.show_boxes and detections: debug_img = img.copy() for det in detections: x1, y1, x2, y2 = det['bbox'] label = f"{det['label']} {det['conf']:.2f}" cv2.rectangle(debug_img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(debug_img, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(self.debug_window, debug_img) cv2.waitKey(1) return targets[:self.max_targets] except Exception as e: print(f"⚠️ 目标检测失败: {str(e)}") return [] def execute_attack_sequence(self): """执行攻击序列""" try: for step in self.attack_sequence: if step['type'] == 'key': keyboard.press(step['key']) time.sleep(step['duration']) keyboard.release(step['key']) elif step['type'] == 'mouse': if step['button'] == 'both': if step['action'] == 'down': pyautogui.mouseDown(button='left') pyautogui.mouseDown(button='right') elif step['action'] == 'up': pyautogui.mouseUp(button='left') pyautogui.mouseUp(button='right') elif step['action'] == 'click': pyautogui.mouseDown(button='left') pyautogui.mouseDown(button='right') time.sleep(0.1) pyautogui.mouseUp(button='left') pyautogui.mouseUp(button='right') else: if step['action'] == 'down': pyautogui.mouseDown(button=step['button']) elif step['action'] == 'up': pyautogui.mouseUp(button=step['button']) elif step['action'] == 'click': pyautogui.click(button=step['button']) time.sleep(step['duration']) return True except Exception as e: print(f"⚠️ 执行攻击序列失败: {str(e)}") return False def safe_attack(self, target): """安全的攻击方法(带攻击序列)""" try: x, y = target['center'] # 添加随机偏移 offset_x = np.random.randint(-5, 5) offset_y = np.random.randint(-5, 5) # 安全的鼠标移动 current_x, current_y = pyautogui.position() # 避免除零错误 dx = max(1, abs(x - current_x)) dy = max(1, abs(y - current_y)) distance = (dx**2 + dy**2)**0.5 # 动态移动时间 move_duration = max(0.05, min(0.3, distance / 2000)) pyautogui.moveTo(x + offset_x, y + offset_y, duration=move_duration) print(f"🎯 锁定目标 {target['class']} @ ({x}, {y}) | 置信度: {target['confidence']:.2f}") # 执行攻击序列 sequence_start = time.time() self.execute_attack_sequence() sequence_time = time.time() - sequence_start # 更新最后检测到目标的时间 self.last_target_time = time.time() # 随机冷却时间(考虑序列执行时间) remaining_cooldown = max(0.1, self.attack_cooldown - sequence_time) time.sleep(remaining_cooldown + np.random.uniform(0, 0.3)) print(f"⚔️ 完成攻击序列 | 耗时: {sequence_time:.2f}s") except Exception as e: print(f"⚠️ 攻击目标失败: {str(e)}") def run(self): """主循环""" if not self.hwnd: self.setup_window() print(f"▶️ 自动化打怪系统启动 | 目标窗口: {self.window_title} | 按ESC退出") self.start_time = time.time() self.frame_count = 0 self.total_inference_time = 0 self.frame_history = [] try: while self.running: if keyboard.is_pressed('esc'): print("⛔ 用户请求退出") self.running = False break frame_start = time.time() self.frame_count += 1 img = self.capture_screen() if img is None: time.sleep(0.1) continue if self.frame_count % 2 == 0: targets = self.get_targets(img) else: targets = [] if targets: for target in targets: self.safe_attack(target) else: time_since_last_target = time.time() - self.last_target_time sleep_time = min(1.0, max(0.1, time_since_last_target * 0.5)) time.sleep(sleep_time) frame_time = time.time() - frame_start if self.frame_count % 5 == 0 and self.frame_count > 0: total_time = time.time() - self.start_time fps = self.frame_count / max(0.001, total_time) inference_percent = (self.total_inference_time / max(0.001, total_time)) * 100
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值