Object Detection and Mouse Macros in FPS Games

写在前面

一年前对于FPS游戏中的作弊技术感到好奇,写了一份DMA的透视外挂,甚至还写了一篇博客“进程数据读取,我好像找到了某个FPS一哥的软肋“来介绍他。当时对于这方面的了解还比较浅薄,不知道直接在原生机器上调用DMA是一种极其落后且愚蠢的行为,导致了两个CS游戏账号全部被封禁。

今年,我又了解到了另一种不那么容易被检测出来的作弊方式,那就是目标检测技术与鼠标宏。 所谓目标检测技术,就是通过深度学习模型(我将要用的是YOLO)或者其他方式对游戏图像进行分析并找到画面中敌人位置的技术; 鼠标宏则是在鼠标层面上而非调用windows api进行编程使得鼠标进行某些自动化行为的技术。很显然,两者都模仿了人类的行为,没有进行任何windows的系统调用,也就不会那么容易被检测出来了。

我将通过实实在在开发一款可用的valorant作弊软件,来介绍下这几种技术以及简单的实现。

不过在开发的过程中,我训练的YOLO模型精度并不能满足我的要求(定位头部,敌我分辨的能力太差),我只会介绍他的流程,最后成品并不会通过这种方式,而是通过另一种我自己的目标检测方案的方式展现。 至于鼠标自动化,由于我没有罗技鼠标,我将用到一块便宜的树莓派Pico进行HID以实现鼠标宏,这块板子我曾经用于开发badusb,这又是一种有趣的hack技术,如果有机会我将会介绍,这里先挂出相关的“项目链接“好了

由于valorant拥有着人工鉴挂流程,太明显的操作(人类无法做到)仍然可能被封禁。所以本文章只提供部分代码以及核心原理作学习使用,请勿用作不良用途,后果自负!

基于YOLO的目标检测模型训练

YOLO是一种流行的目标检测模型,并且支持在其基础模型上通过自己的数据集再训练。

该模型的输出是图中所有属于某分类的检测框左上角坐标以及长宽xywh,可视化的效果如下图所示

我检索到了以下数据集并尝试着训练:

cs2的阵营、头身分类数据集:Counter Strike 2 Body and Head Classification

valorant的敌我、头身分类数据集,拉伸至416x416大小:Valorant Object Detection Object Detection Dataset and Pre-Trained Model by Valorant Object Detection

可惜的是效果并不理想,精度差的同时检测结果在不同帧会抖动(我没有加入滤波),甚至没有下面将要提到的颜色特征方式来的有效。我的想法是这种基于模型的检测方法可能不大适用于fps这种高精度且需要快速反应的游戏。当然也可能是因为数据集或者我本身数据预处理的问题,所以当有空或发现较好的数据集时我也会进行验证与更新。

不过这种方式对没有gpu支持的计算机不大友好,在我那台只有集显的笔记本上每一帧有200ms左右的延迟,所以暂时我先考虑颜色特征的方法了。

基于颜色特征的作弊方式

自动架点

自动架点的原理非常简单,当按下自动架点的按键后程序截取准心附近的区域R作为原始图像,然后实时截图计算R区域与原始图像相比的平均像素颜色变化,如果大于一个阈值,就说明了有人经过,此时便可以开枪了。

当然,队友经过或者道具覆盖也会触发,这就是这种基于像素变化架点方式的不足了。经过cs2完美平台反应测试能有100ms左右的反应,远超职业选手的水平,足够实战中架防守狙使用了。

class AutoTrigger:
    def __init__(self,ser,threshold,region_size,delay):
        self.last_avg_color = None
        
        self.ser = ser
        self.threshold = threshold
        self.region_size = region_size
        self.delay = delay

    def calculate_avg_color(self):
        with mss.mss() as sct:
            self.center_x = sct.monitors[1]["width"] // 2
            self.center_y = sct.monitors[1]["height"] // 2
            region = {
                "left": self.center_x - self.region_size // 2,
                "top": self.center_y - self.region_size // 2,
                "width": self.region_size,
                "height": self.region_size,
            }
            img = sct.grab(region)
            img_array = np.array(img)
            return np.mean(img_array, axis=(0, 1))

    def check_color_change(self, current_avg):
        if self.last_avg_color is None:return False
        color_diff = np.abs(current_avg - self.last_avg_color)
        return np.any(color_diff > self.threshold)

    def run(self):
        self.last_avg_color = self.calculate_avg_color()
        while True:
            current_avg = self.calculate_avg_color()
            if self.check_color_change(current_avg):
                time.sleep(self.delay/1000)
                self.ser.write(b"9999,9999\n")
                self.ser.flush()
                self.active = False
                break

自动瞄准

自动瞄准的原理说起来其实也十分简单。上面说到YOLO的目标检测因为数据集问题精度不够高,而且在低性能计算机上延迟较高。直接触发了我打智能车比赛以来的被动——非黑盒目标检测。说白了就是人眼判断特征写到代码里。

我暂时只针对Valrant写了这种辅助,因为这款游戏的敌人具有高亮色。我找出高亮色所在的色域区间,通过色域区间将屏幕中不属于该区间的点全部过滤掉,剩下的就全是敌人所在的区域了。对敌人所在的区域找到最高点,加个小的向下偏移就是头部的位置了。

经过测试在实战中约有60%-70%的爆头率,已经远高于普通人的水平。由于环境色影响,敌人头部颜色有时因为距离淡出色域等原因,暂时无法将爆头率进一步提高。不过将来我可以通过调整色域以及加入更多特征分析(如区域面积)改进这一问题。

总而言之,这种目标检测方式已经取得了小小的成果,并且照顾非gpu环境的计算机(集显笔记本),我个人还是非常满意的。

class AutoAim:
    def __init__(self,ser,size,lColor,hColor,sens,offsets):
        self.ser = ser
        self.size = size
        self.lColor = lColor
        self.hColor = hColor
        self.sens = sens
        self.offsets = offsets

    def run(self):
        lower_bound = np.clip(self.lColor, 0, 255)
        upper_bound = np.clip(self.hColor, 0, 255)

        with mss.mss() as sct:
            screen_width = sct.monitors[1]['width']
            screen_height = sct.monitors[1]['height']
        
            region = {
                "top": screen_height // 2 - self.size//2,
                "left": screen_width // 2 - self.size//2,
                "width": self.size,
                "height": self.size
            }
            screenshot = sct.grab(region)
            frame = np.array(screenshot)
            frame = cv2.cvtColor(frame, cv2.COLOR_BGRA2BGR) 
        
            mask = cv2.inRange(frame, lower_bound, upper_bound)
            contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        
            if contours:
                min_x, min_y = self.size, self.size
                for contour in contours:
                    x, y, _, _ = cv2.boundingRect(contour)
                    if y < min_y or (y == min_y and x < min_x):
                        min_x, min_y = x, y

                rel_x = min_x - self.size//2
                rel_y = min_y - self.size//2

                print(f"Find left top key point, sent ({rel_x}, {rel_y})")
                data = f"{int((rel_x+self.offsets[0])*(0.812/self.sens))},{int((rel_y+self.offsets[1])*(0.812/self.sens))}\n".encode()
                self.ser.write(data)
                self.ser.flush()
            else:
                self.ser.write(b"0,0\n")
                self.ser.flush()
                print("No key point detected, sent (0,0)")

通过树莓派Pico实现鼠标宏

注意要买有引脚版本的,因为除了自带usb接口外还需要再使用usb转ttl再接出一个usb出来,分别用于串口和HID

开发板示意图:

为什么使用这种方式?原因之一是我手头没有罗技鼠标,其它单片机也大多被我带回了家;其二是该板子成本很低,只有15元左右,而且支持circuit python或micro python开发,可以实现上下位机开发语言一致。

整个实现流程我可以用下图表示出来:

整个流程的核心是 计算机程序进行图像处理和决策,然后通过 串口通信 发送信号到 树莓派,树莓派解析信号后控制 HID鼠标 执行相应操作,如自动架点、自动瞄准等。

可以发现计算机端调用的截图,串口通信,HID设备都不是游戏防作弊的常规检测项,因为正常游戏流程也需要用到这些。

流程和原理很清晰明了,代码实现倒是不怎么重要了,我一直认为代码只是实现功能的工具罢了。所以这里只挂出大致代码

uart = busio.UART(board.GP4, board.GP5, baudrate=9600)
mouse = Mouse(usb_hid.devices)

while True:
    data = uart.readline()
    if data:
        try:
            message = data.decode("utf-8").strip()
            print(f"Received data: {message}")
            
            if message and "," in message:
                x, y = map(int, message.split(","))
                
                if x == 0 and y == 0:
                    print("No target detected, skipping movement.")
                    continue
                elif x == 9999 and y == 9999:
                    mouse.click(Mouse.LEFT_BUTTON)
                    print("Mouse clicked! Be triggerred success!")
                    continue
                
                smooth_move(mouse, x, y)
                
                mouse.click(Mouse.LEFT_BUTTON)
                print(f"Mouse moved smoothly to {x}, {y} and clicked.")
                
            else:
                print("Error: Invalid or empty data received")
                
        except Exception as e:
            print(f"Error: Failed to decode or parse the received data. Exception: {e}")
    else:
        time.sleep(0.001)

具体请参考我仓库中的配置方法与实现代码一步步完成整体的部署吧。

后记

项目地址:https://github.com/SugarSong404/fps-cheat 现在已有发行版可供测试。

这方面玩法还很多,比如说未来有机会我可能会考虑通过FPGA的方式实现DMA,又或者是提高目标检测模型的精度与速度。不过都是后话了。有想法的人欢迎一起交流,我的微信号:TangSong404。