Python Apex Legends 武器自动识别与压枪 全过程记录

博文目录


本文为下面参考文章的学习与实践

[原文] FPS游戏自动枪械识别+压枪(以PUBG为例)
[转载] FPS游戏自动枪械识别+压枪(以PUBG为例)

环境准备

Python Windows 开发环境搭建

conda create -n apex python=3.9

操纵键鼠

由于绝地求生屏蔽了硬件驱动外的其他鼠标输入,因此我们无法直接通过py脚本来控制游戏内鼠标操作。为了实现游戏内的鼠标下移,我使用了罗技鼠标的驱动(ghub),而py通过调用ghub的链接库文件,将指令操作传递给ghub,最终实现使用硬件驱动的鼠标指令输入给游戏,从而绕过游戏的鼠标输入限制。值得一提的是,我们只是通过py代码调用链接库的接口将指令传递给罗技驱动的,跟实际使用的是何种鼠标没有关系,所以即便用户使用的是雷蛇、卓威、双飞燕等鼠标,对下面的代码并无任何影响。

驱动安装 链接库加载 代码准备和游戏外测试

罗技驱动使用 LGS_9.02.65_X64(请自行找资源安装,官网新版罗技驱动没找到对应的链接库文件),链接库文件在项目链接里面可以找到。下面是载入链接库的代码。

罗技驱动分LGS(老)和GHub(新), 必须装指定版本的LGS驱动(如已安装GHub可能需要卸载), 不然要么报未安装, 要么初始化成功但调用无效

LGS_9.02.65_x64_Logitech.exe, 网盘下载

mouse.device.lgs.dll

try:
    gm = CDLL(r'./mouse.device.lgs.dll')
    gmok = gm.device_open() == 1
    if not gmok:
        print('未安装ghub或者lgs驱动!!!')
    else:
        print('初始化成功!')
except FileNotFoundError:
    print('缺少文件')

装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 这个 dll 文件里面的方法都没有对应的文档, 只能猜测参数了

toolkit.py

import time
from ctypes import CDLL

import win32api  # conda install pywin32


try:
    driver = CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化失败, 缺少文件')


class Mouse:

    @staticmethod
    def move(x, y, absolute=False):
        if ok:
            mx, my = x, y
            if absolute:
                ox, oy = win32api.GetCursorPos()
                mx = x - ox
                my = y - oy
            driver.moveR(mx, my, True)

    @staticmethod
    def down(code):
        if ok:
            driver.mouse_down(code)

    @staticmethod
    def up(code):
        if ok:
            driver.mouse_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
        :return:
        """
        if ok:
            driver.mouse_down(code)
            driver.mouse_up(code)


class Keyboard:

    @staticmethod
    def press(code):
        if ok:
            driver.key_down(code)

    @staticmethod
    def release(code):
        if ok:
            driver.key_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
        :return:
        """
        if ok:
            driver.key_down(code)
            driver.key_up(code)

游戏内测试

在游戏里面试过后, 管用, 但是不准, 猜测可能和游戏内鼠标灵敏度/FOV等有关系

from toolkit import Mouse
import pynput  # conda install pynput

def onClick(x, y, button, pressed):
    if not pressed:
        if pynput.mouse.Button.x2 == button:
            Mouse.move(100, 100)


mouseListener = pynput.mouse.Listener(on_click=onClick)
mouseListener.start()
mouseListener.join()

键鼠监听

前面说到,要实现压枪就要对各种配件、状态做出识别。那么在写识别的函数之前,我们先要解决的是何时识别的问题。如果识别使用多线程\多进程的一直持续检测,无疑是一种巨大的开销,因此就需要对键盘、鼠标的状态进行监听。只有按下特定按键时,才触发特定相应的识别请求。

这里我使用的钩子是Pynput,其他可使用的库还有Pyhook3

Pynput 说明

def onClick(x, y, button, pressed):
    print(f'button {
      
      button} {
      
      "pressed" if pressed else "released"} at ({
      
      x},{
      
      y})')
    if pynput.mouse.Button.left == button:
        return False  # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.mouse.Listener(on_click=onClick)
listener.start()


def onRelease(key):
    print(f'{
      
      key} released')
    if key == pynput.keyboard.Key.end:
        return False  # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.keyboard.Listener(on_release=onRelease)
listener.start()

注意调试回调方法的时候, 不要打断点, 不要打断点, 不要打断点, 这样会卡死IO, 导致鼠标键盘失效

Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False

键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法

有些键不知道该怎么写, 可以 print(key) 查看写法

这里有一点非常坑,on_press和on_release的参数只能有一个key,这个key就是对应键盘按下的哪颗按键。但这是不足以满足我们的需求的,因为我们应该在钩子函数内部,在按下指定按键时对信号量做出修改,但因为参数的限制,我们无法把信号量传进函数内部,这里我也是想了很久,最后才想到用嵌套函数的写法解决这个问题。

另外,钩子函数本身是阻塞的。也就是说钩子函数在执行的过程中,用户正常的键盘/鼠标操作是无法输入的。所以在钩子函数里面必须写成有限的操作(即O(1)时间复杂度的代码),也就是说像背包内配件及枪械识别,还有下文会讲到的鼠标压枪这类时间开销比较大或者持续时间长的操作,都不适合写在钩子函数里面。这也解释了为什么在检测到Tab(打开背包)、鼠标左键按下时,为什么只是改变信号量,然后把这些任务丢给别的进程去做的原因。

武器识别

在这里插入图片描述

如何简单且高效地判断是否在游戏内

先是判断游戏窗体是否在最前端, 然后判断游戏内是否正在持枪界面

找几个特征点取色判断, 血条左上角和生存物品框左下角

一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定

我原本以为屏幕点取色应该不会超过1ms的耗时, 结果万万没想到, 取个色居然要1-10ms, 效率奇低, 暂无其他优雅方法

如何简单且高效地判断背包状态 无武器/1号武器/2号武器

在这里插入图片描述

看武器边框上红色圈住的部分颜色, 灰色说明没有武器, 上下不同色说明使用2号武器, 上下同色说明使用1号武器

如何简单且高效地判断武器子弹类别 轻型/重型/能量/狙击/霰弹/空投

因为不同子弹类型的武器, 边框颜色不一样. 所以可以和上面的放在一起, 同一个点直接判断出背包状态和子弹类别

如何简单且高效地判断武器名称

在分类后的基础上, 通过 背包状态 确定要检查颜色的位置(1号位/2号位), 通过 武器子弹类别 缩小判断范围, 在每个武器的名字上找一个纯白色的点, 确保这个点只有这把武器是纯白色, 然后逐个对比

如何简单且高效地判断武器模式 全自动/连发/单发

在这里插入图片描述
需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(后面有可能做自动单发, 到时候在考虑), 喷子和狙不需要压枪

所以需要找一个能区分三种模式的点(不同模式这个点的颜色不同但是稳定), 且这个点不能受和平和三重的特殊标记影响

一开始找了个不是纯白色的点, 后来发现这个点的颜色会被背景颜色影响到, 不是恒定不变的. 最终还是放弃了一个点即可分辨模式的想法, 采用了稳妥的纯白色点, 保证该点只在该模式下是纯白色的, 在其他模式都不是纯白色即可

如何简单且高效地判断是否持有武器

暂无法判断, 收起武器和持有武器, 没有能明确分辨两种情况的固定点

部分武器可以通过[V]标判断, 因为不全, 先不采用

也可以通过监听按按[3]键(收起武器操作)来设置标记, 其他操作去除标记, 然后通过读取该标记判断是否持有武器, 但不优雅, 先不采用

目前已有的结论是, 使用拳头时, 准星是一个大号方形准星, 使用武器时, 都是圆准星. 但是使用拳头不等于未持有武器

如何简单且高效地判断弹夹是否打空

在这里插入图片描述
弹夹中子弹数大多为两位数(LSTAR可能为三位数), 所以只需确认十位不为0, 即可认为不空, 十位为0且个位为0, 即可认为空

  • 十位的点, 在数字正中间即可, 1-9都是纯白色, 0是灰色. 注意, 这个灰色不是定色, 该颜色会随着背景改变而改变
  • 个位的点, 在数字0中间斜线的最左端, 这个点是纯白色, 且其他1-9时, 这个点都不是纯白色

何时触发识别

  • 鼠标右键 按下, 识别武器. 和游戏内原本的按键功能不冲突
  • 1/2/3/E/V/R/Tab/Esc/Alt 键释放, 识别武器
  • Home 键释放, 切换开关
  • end 键释放, 结束程序

几个细节点

  • 通过测试发现, 所有武器的发射间隔都大于50毫秒, 所以压枪时, 这50毫秒内可以做一些操作, 比如判断弹夹是否打空, 避免触发压枪

压枪思路

apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??

  • 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
  • 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
    可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦
    这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱
  • 还有就是现在很火的AI目标检测(yolov5), 我也有尝试做, 环境搭好了, 但是中途卡住了. 一是毕竟python是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊. 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …

组织数据

武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息

配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息分类

信号数据, 程序运行时, 进程线程间通讯

第一阶段实现 能自动识别出所有武器

目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不会超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了

我的配置: AMD R7 2700x, Nvidia RTX 2080, 3440*1440 分辨率

cfg.py


mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'  # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet'  # 子弹
differ = 'differ'
positive = 'positive'  # 肯定的
negative = 'negative'  # 否定的

# 检测数据
detect = {
    
    
    "3440:1440": {
    
    
        game: [  # 判断是否在游戏中
            {
    
    
                point: (236, 1344),  # 点的坐标, 血条左上角
                color: 0x00FFFFFF  # 点的颜色, 255, 255, 255
            },
            {
    
    
                point: (2692, 1372),  # 生存物品右下角
                color: 0x959595  # 149, 149, 149
            }
        ],
        pack: {
    
      # 背包状态, 有无武器, 选择的武器
            point: (2900, 1372),  # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
            color: 0x808080,  # 无武器时, 灰色, 128, 128, 128
            '0x447bb4': 1,  # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
            '0x839b54': 2,  # 重型弹药武器
            '0x3da084': 3,  # 能量弹药武器
            '0xce5f6e': 4,  # 狙击弹药武器
            '0xf339b': 5,  # 霰弹枪弹药武器
            '0x5302ff': 6,  # 空投武器
        },
        mode: {
    
      # 武器模式, 全自动/半自动/单发/其他
            point: (3148, 1349),
            '0xf8f8f8': 1,  # 全自动
            '0xfefefe': 2  # 半自动
        },
        name: {
    
      # 武器名称判断
            color: 0x00FFFFFF,
            '1': {
    
      # 1号武器
                '1': [  # 轻型弹药武器
                    (2959, 1386),  # 1: RE-45 自动手枪
                    (2970, 1385),  # 2: 转换者冲锋枪
                    (2972, 1386),  # 3: R-301 卡宾枪
                    (2976, 1386),  # 4: R-99 冲锋枪
                    (2980, 1386),  # 5: P2020 手枪
                    (2980, 1384),  # 6: 喷火轻机枪
                    (2987, 1387),  # 7: G7 侦查枪
                    (3015, 1386),  # 8: CAR (轻型弹药)
                ],
                '2': [  # 重型弹药武器
                    (2957, 1385),  # 1: 赫姆洛克突击步枪
                    (2982, 1385),  # 2: 猎兽冲锋枪
                    (2990, 1393),  # 3: 平行步枪
                    (3004, 1386),  # 4: 30-30
                    (3015, 1386),  # 5: CAR (重型弹药)
                ],
                '3': [  # 能量弹药武器
                    (2955, 1386),  # 1: L-STAR能量机枪
                    (2970, 1384),  # 2: 三重式狙击枪
                    (2981, 1385),  # 3: 电能冲锋枪
                    (2986, 1384),  # 4: 专注轻机枪
                    (2980, 1384),  # 5: 哈沃克步枪
                ],
                '4': [  # 狙击弹药武器
                    (2969, 1395),  # 1: 哨兵狙击步枪
                    (2999, 1382),  # 2: 充能步枪
                    (2992, 1385),  # 3: 辅助手枪
                    (3016, 1383),  # 4: 长弓
                ],
                '5': [  # 霰弹枪弹药武器
                    (2957, 1384),  # 1: 和平捍卫者霰弹枪
                    (2995, 1382),  # 2: 莫桑比克
                    (3005, 1386),  # 3: EVA-8
                ],
                '6': [  # 空投武器
                    (2958, 1384),  # 1: 克雷贝尔狙击枪
                    (2983, 1384),  # 2: 敖犬霰弹枪
                    (3003, 1383),  # 3: 波塞克
                    (3014, 1383),  # 4: 暴走
                ]
            },
            '2': {
    
    
                differ: 195  # 直接用1的坐标, 横坐标右移195就可以了
            }
        }
    },
    "2560:1440": {
    
    

    },
    "2560:1080": {
    
    

    },
    "1920:1080": {
    
    

    }
}

# 武器数据
weapon = {
    
    
    '1': {
    
      # 轻型弹药武器
        '1': {
    
    
            name: 'RE-45 自动手枪',
        },
        '2': {
    
    
            name: '转换者冲锋枪',
        },
        '3': {
    
    
            name: 'R-301 卡宾枪',
        },
        '4': {
    
    
            name: 'R-99 冲锋枪',
        },
        '5': {
    
    
            name: 'P2020 手枪',
        },
        '6': {
    
    
            name: '喷火轻机枪',
        },
        '7': {
    
    
            name: 'G7 侦查枪',
        },
        '8': {
    
    
            name: 'CAR (轻型弹药)',
        }
    },
    '2': {
    
      # 重型弹药武器
        '1': {
    
    
            name: '赫姆洛克突击步枪',
        },
        '2': {
    
    
            name: '猎兽冲锋枪',
        },
        '3': {
    
    
            name: '平行步枪',
        },
        '4': {
    
    
            name: '30-30',
        },
        '5': {
    
    
            name: 'CAR (重型弹药)',
        }
    },
    '3': {
    
      # 能量弹药武器
        '1': {
    
    
            name: 'L-STAR能量机枪',
        },
        '2': {
    
    
            name: '三重式狙击枪',
        },
        '3': {
    
    
            name: '电能冲锋枪',
        },
        '4': {
    
    
            name: '专注轻机枪',
        },
        '5': {
    
    
            name: '哈沃克步枪',
        },
    },
    '4': {
    
      # 狙击弹药武器
        '1': {
    
    
            name: '哨兵狙击步枪',
        },
        '2': {
    
    
            name: '充能步枪',
        },
        '3': {
    
    
            name: '辅助手枪',
        },
        '4': {
    
    
            name: '长弓',
        },
    },
    '5': {
    
      # 霰弹弹药武器
        '1': {
    
    
            name: '和平捍卫者霰弹枪',
        },
        '2': {
    
    
            name: '莫桑比克',
        },
        '3': {
    
    
            name: 'EVA-8',
        },
    },
    '6': {
    
      # 空投武器
        '1': {
    
    
            name: '克雷贝尔狙击枪',
        },
        '2': {
    
    
            name: '敖犬霰弹枪',
        },
        '3': {
    
    
            name: '波塞克',
        },
        '4': {
    
    
            name: '暴走',
        },
    }
}

toolkit.py

import mss  # pip install mss
import ctypes

from ctypes import CDLL

import cfg
from cfg import detect, weapon

# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
hdc = user32.GetDC(None)

try:
    driver = CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化失败, 缺少文件')


class Mouse:

    @staticmethod
    def point():
        return user32.GetCursorPos()

    @staticmethod
    def move(x, y, absolute=False):
        if ok:
            mx, my = x, y
            if absolute:
                ox, oy = user32.GetCursorPos()
                mx = x - ox
                my = y - oy
            driver.moveR(mx, my, True)

    @staticmethod
    def moveHumanoid(x, y, absolute=False):
        """
        仿真移动(还没做好)
        """
        if ok:
            ox, oy = user32.GetCursorPos()  # 原鼠标位置
            mx, my = x, y  # 相对移动距离
            if absolute:
                mx = x - ox
                my = y - oy
            tx, ty = ox + mx, oy + my
            print(f'({
      
      ox},{
      
      oy}), ({
      
      tx},{
      
      ty}), x:{
      
      mx},y:{
      
      my}')
            # 以绝对位置方式移动(防止相对位置丢失精度)
            adx, ady = abs(mx), abs(my)
            if adx <= ady:
                # 水平方向移动的距离短
                for i in range(1, adx):
                    ix = i if mx > 0 else -i
                    temp = int(ady / adx * abs(ix))
                    iy = temp if my > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)
            else:
                # 垂直方向移动的距离短
                for i in range(1, ady):
                    iy = i if my > 0 else -i
                    temp = int(adx / ady * abs(iy))
                    ix = temp if mx > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)

    @staticmethod
    def down(code):
        if ok:
            driver.mouse_down(code)

    @staticmethod
    def up(code):
        if ok:
            driver.mouse_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
        :return:
        """
        if ok:
            driver.mouse_down(code)
            driver.mouse_up(code)


class Keyboard:

    @staticmethod
    def press(code):
        if ok:
            driver.key_down(code)

    @staticmethod
    def release(code):
        if ok:
            driver.key_up(code)

    @staticmethod
    def click(code):
        """
        键盘按键函数中,传入的参数采用的是键盘按键对应的键码
        :param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
        :return:
        """
        if ok:
            driver.key_down(code)
            driver.key_up(code)


class Monitor:
    """
    显示器
    """
    sct = mss.mss()

    @staticmethod
    def grab(region):
        """
        region: tuple, (left, top, width, height)
        pip install mss
        """
        left, top, width, height = region
        return Monitor.sct.grab(monitor={
    
    'left': left, 'top': top, 'width': width, 'height': height})

    @staticmethod
    def pixel(x, y):
        """
        效率很低且不稳定, 单点检测都要耗时1-10ms
        获取颜色, COLORREF 格式, 0x00FFFFFF
        结果是int,
        可以通过 print(hex(color)) 查看十六进制值
        可以通过 print(color == 0x00FFFFFF) 进行颜色判断
        """
        # hdc = user32.GetDC(None)
        return gdi32.GetPixel(hdc, x, y)

    class Resolution:
        """
        分辨率
        """

        @staticmethod
        def display():
            """
            显示分辨率
            """
            w = user32.GetSystemMetrics(0)
            h = user32.GetSystemMetrics(1)
            return w, h

        @staticmethod
        def virtual():
            """
            多屏幕组合的虚拟显示器分辨率
            """
            w = user32.GetSystemMetrics(78)
            h = user32.GetSystemMetrics(79)
            return w, h

        @staticmethod
        def physical():
            """
            物理分辨率
            """
            # hdc = user32.GetDC(None)
            w = gdi32.GetDeviceCaps(hdc, 118)
            h = gdi32.GetDeviceCaps(hdc, 117)
            return w, h


class Game:
    """
    游戏工具
    """

    @staticmethod
    def game():
        """
        是否在游戏内
        太耗时了, 所以不能调的多了
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.game)
        for item in data:
            x, y = item.get(cfg.point)
            if Monitor.pixel(x, y) != item.get(cfg.color):
                return False
        return True

    @staticmethod
    def index():
        """
        武器索引和子弹类型索引
        :return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
                 子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.pack)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        if data.get(cfg.color) == color:
            return None, None
        else:
            bullet = data.get(hex(color))
            return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)

    @staticmethod
    def weapon(index, bullet):
        """
        通过武器位和子弹类型识别武器, 参考:config.detect.name
        :param index: 武器位, 1:1号位, 2:2号位
        :param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
        :return:
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.name)
        color = data.get(cfg.color)
        if index == 1:
            lst = data.get(str(index)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x, y):
                    return i + 1
        elif index == 2:
            differ = data.get(str(index)).get(cfg.differ)
            lst = data.get(str(1)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x + differ, y):
                    return i + 1
        return None

    @staticmethod
    def mode():
        """
        武器模式
        :return:  1:全自动, 2:半自动, None:其他
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.mode)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        return data.get(hex(color))

    @staticmethod
    def detect():
        """
        决策是否需要压枪, 向信号量写数据
        """
        if Game.game() is False:
            print('not in game')

            return
        index, bullet = Game.index()
        if (index is None) | (bullet is None):
            print('no weapon')

            return
        if Game.mode() is None:
            print('not in full auto or semi auto mode')

            return
        arms = Game.weapon(index, bullet)
        if arms is None:
            print('detect weapon failure')

            return
        # 检测通过, 需要压枪
        print(weapon.get(str(bullet)).get(str(arms)).get(cfg.name))
        return weapon.get(str(bullet)).get(str(arms)).get(cfg.name)

apex.py

import time

import pynput  # conda install pynput

import toolkit

ExitFlag = False


def down(x, y, button, pressed):
    global ExitFlag
    if ExitFlag:
        print(ExitFlag)
        return False  # 结束监听线程
    if pressed:  # 按下
        if pynput.mouse.Button.right == button:
            toolkit.Game.detect()


mouseListener = pynput.mouse.Listener(on_click=down)
mouseListener.start()


def release(key):
    if key == pynput.keyboard.Key.end:
        print('end')
        global ExitFlag
        ExitFlag = True
        return False
    if key == pynput.keyboard.KeyCode.from_char('1'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('2'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('3'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('e'):
        toolkit.Game.detect()
    elif key == pynput.keyboard.KeyCode.from_char('v'):
        toolkit.Game.detect()


keyboardListener = pynput.keyboard.Listener(on_release=release)
keyboardListener.start()
keyboardListener.join()

在这里插入图片描述

第二阶段实现 能自动采用对应抖枪参数执行压枪

在这里插入图片描述

  • 游戏内鼠标灵敏度越高越容易抖枪且效果更好, 但是开到5的话, 会感到有点晕
  • 游戏内鼠标灵敏度越高, 代码里抖动的像素就需要设置的更小, 比如5的灵敏度, 抖动2像素就可以了
  • 抖枪能减小后坐力, 但不能完全消除, 所以还需配合对应方向的移动

在2.5灵敏度下, 301使用下面这个参数, 二三十米还行, 五十米, 三倍效果还将就一倍就很差了. 后坐力越大的武器, 前几枪容易跳太高, 下压力度可以大点

还有就是延迟要低一点, 我这边裸连延迟300+, 经常子弹打出去, 过半秒才减血, 这样很难测的准

能量武器, 专注和哈沃克, 预热和涡轮有很大影响, 这里没管, 将就将就

total = 0  # 总计时 ms
delay = 1  # 延迟 ms
pixel = 4  # 抖动像素
while True:
    if not data[fire]:
        break
    # 下压
    if total < 30:
        toolkit.Mouse.move(0, 5)
        time.sleep(delay / 1000)
        total += delay
    else:
        toolkit.Mouse.move(0, 1)
        time.sleep(delay / 1000)
        total += delay
    # 抖枪
   	toolkit.Mouse.move(pixel, 0)
    time.sleep(delay / 1000)
    total += delay
    toolkit.Mouse.move(-pixel, 0)
    time.sleep(delay / 1000)
    total += delay

cfg.py

mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'  # 背包
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
bullet = 'bullet'  # 子弹
differ = 'differ'
suppress = 'suppress'
strength = 'strength'
positive = 'positive'  # 肯定的
negative = 'negative'  # 否定的

# 检测数据
detect = {
    
    
    "3440:1440": {
    
    
        game: [  # 判断是否在游戏中
            {
    
    
                point: (236, 1344),  # 点的坐标, 血条左上角
                color: 0x00FFFFFF  # 点的颜色, 255, 255, 255
            },
            {
    
    
                point: (2692, 1372),  # 生存物品右下角
                color: 0x959595  # 149, 149, 149
            }
        ],
        pack: {
    
      # 背包状态, 有无武器, 选择的武器
            point: (2900, 1372),  # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
            color: 0x808080,  # 无武器时, 灰色, 128, 128, 128
            '0x447bb4': 1,  # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
            '0x839b54': 2,  # 重型弹药武器
            '0x3da084': 3,  # 能量弹药武器
            '0xce5f6e': 4,  # 狙击弹药武器
            '0xf339b': 5,  # 霰弹枪弹药武器
            '0x5302ff': 6,  # 空投武器
        },
        mode: {
    
      # 武器模式, 全自动/半自动/单发/其他
            color: 0x00FFFFFF,
            '1': (3151, 1347),  # 全自动
            '2': (3171, 1351),  # 半自动
        },
        name: {
    
      # 武器名称判断
            color: 0x00FFFFFF,
            '1': {
    
      # 1号武器
                '1': [  # 轻型弹药武器
                    (2959, 1386),  # 1: RE-45 自动手枪
                    (2970, 1385),  # 2: 转换者冲锋枪
                    (2972, 1386),  # 3: R-301 卡宾枪
                    (2976, 1386),  # 4: R-99 冲锋枪
                    (2980, 1386),  # 5: P2020 手枪
                    (2980, 1384),  # 6: 喷火轻机枪
                    (2987, 1387),  # 7: G7 侦查枪
                    (3015, 1386),  # 8: CAR (轻型弹药)
                ],
                '2': [  # 重型弹药武器
                    (2957, 1385),  # 1: 赫姆洛克突击步枪
                    (2982, 1385),  # 2: 猎兽冲锋枪
                    (2990, 1393),  # 3: 平行步枪
                    (3004, 1386),  # 4: 30-30
                    (3015, 1386),  # 5: CAR (重型弹药)
                ],
                '3': [  # 能量弹药武器
                    (2955, 1386),  # 1: L-STAR能量机枪
                    (2970, 1384),  # 2: 三重式狙击枪
                    (2981, 1385),  # 3: 电能冲锋枪
                    (2986, 1384),  # 4: 专注轻机枪
                    (2980, 1384),  # 5: 哈沃克步枪
                ],
                '4': [  # 狙击弹药武器
                    (2969, 1395),  # 1: 哨兵狙击步枪
                    (2999, 1382),  # 2: 充能步枪
                    (2992, 1385),  # 3: 辅助手枪
                    (3016, 1383),  # 4: 长弓
                ],
                '5': [  # 霰弹枪弹药武器
                    (2957, 1384),  # 1: 和平捍卫者霰弹枪
                    (2995, 1382),  # 2: 莫桑比克
                    (3005, 1386),  # 3: EVA-8
                ],
                '6': [  # 空投武器
                    (2958, 1384),  # 1: 克雷贝尔狙击枪
                    (2983, 1384),  # 2: 敖犬霰弹枪
                    (3003, 1383),  # 3: 波塞克
                    (3014, 1383),  # 4: 暴走
                ]
            },
            '2': {
    
    
                differ: 195  # 直接用1的坐标, 横坐标右移195就可以了
            }
        }
    },
    "2560:1440": {
    
    

    },
    "2560:1080": {
    
    

    },
    "1920:1080": {
    
    

    }
}

# 武器数据
weapon = {
    
    
    '1': {
    
      # 轻型弹药武器
        '1': {
    
    
            name: 'RE-45 自动手枪',  # 全程往右飘
            shake: {
    
    
                speed: 80,
                count: 10,
                strength: 5,
            }
        },
        '2': {
    
    
            name: '转换者冲锋枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 7,
            }
        },
        '3': {
    
    
            name: 'R-301 卡宾枪',
            shake: {
    
    
                speed: 74,  # 74ms打一发子弹
                count: 6,  # 压制前6发
                strength: 5,  # 压制的力度(下移的像素)
            },
            suppress: {
    
    
                speed: 74,
            }
        },
        '4': {
    
    
            name: 'R-99 冲锋枪',
            shake: {
    
    
                speed: 55.5,
                count: 13,
                strength: 8,
            }
        },
        '5': {
    
    
            name: 'P2020 手枪',
        },
        '6': {
    
    
            name: '喷火轻机枪',
            shake: {
    
    
                speed: 111,
                count: 8,
                strength: 5,
            }
        },
        '7': {
    
    
            name: 'G7 侦查枪',
        },
        '8': {
    
    
            name: 'CAR (轻型弹药)',
            shake: {
    
    
                speed: 64.5,
                count: 10,
                strength: 7,
            }
        }
    },
    '2': {
    
      # 重型弹药武器
        '1': {
    
    
            name: '赫姆洛克突击步枪',
            shake: {
    
    
                speed: 50,
                count: 3,
                strength: 6,
            }
        },
        '2': {
    
    
            name: '猎兽冲锋枪',
            shake: {
    
    
                speed: 50,
                count: 5,
                strength: 6,
            }
        },
        '3': {
    
    
            name: '平行步枪',
            shake: {
    
    
                speed: 100,
                count: 5,
                strength: 5,
            }
        },
        '4': {
    
    
            name: '30-30',
        },
        '5': {
    
    
            name: 'CAR (重型弹药)',
            shake: {
    
    
                speed: 64.5,
                count: 10,
                strength: 7,
            }
        }
    },
    '3': {
    
      # 能量弹药武器
        '1': {
    
    
            name: 'L-STAR能量机枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 5,
            }
        },
        '2': {
    
    
            name: '三重式狙击枪',
        },
        '3': {
    
    
            name: '电能冲锋枪',
            shake: {
    
    
                speed: 83.3,
                count: 10,
                strength: 7,
            }
        },
        '4': {
    
    
            name: '专注轻机枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 7,
            }
        },
        '5': {
    
    
            name: '哈沃克步枪',
            shake: {
    
    
                speed: 100,
                count: 8,
                strength: 6,
            }
        },
    },
    '4': {
    
      # 狙击弹药武器
        '1': {
    
    
            name: '哨兵狙击步枪',
        },
        '2': {
    
    
            name: '充能步枪',
        },
        '3': {
    
    
            name: '辅助手枪',
        },
        '4': {
    
    
            name: '长弓',
        },
    },
    '5': {
    
      # 霰弹弹药武器
        '1': {
    
    
            name: '和平捍卫者霰弹枪',
        },
        '2': {
    
    
            name: '莫桑比克',
        },
        '3': {
    
    
            name: 'EVA-8',
        },
    },
    '6': {
    
      # 空投武器
        '1': {
    
    
            name: '克雷贝尔狙击枪',
        },
        '2': {
    
    
            name: '敖犬霰弹枪',
        },
        '3': {
    
    
            name: '波塞克',
        },
        '4': {
    
    
            name: '暴走',
            shake: {
    
    
                speed: 200,
                count: 8,
                strength: 2,
            }
        },
    }
}

toolkit.py

import time

import mss  # pip install mss
import ctypes

from ctypes import CDLL

import cfg
from cfg import detect, weapon

# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32

try:
    driver = CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化失败, 缺少文件')


class Mouse:

    @staticmethod
    def point():
        return user32.GetCursorPos()

    @staticmethod
    def move(x, y, absolute=False):
        if ok:
            mx, my = x, y
            if absolute:
                ox, oy = user32.GetCursorPos()
                mx = x - ox
                my = y - oy
            driver.moveR(mx, my, True)

    @staticmethod
    def moveHumanoid(x, y, absolute=False):
        """
        仿真移动(还没做好)
        """
        if ok:
            ox, oy = user32.GetCursorPos()  # 原鼠标位置
            mx, my = x, y  # 相对移动距离
            if absolute:
                mx = x - ox
                my = y - oy
            tx, ty = ox + mx, oy + my
            print(f'({
      
      ox},{
      
      oy}), ({
      
      tx},{
      
      ty}), x:{
      
      mx},y:{
      
      my}')
            # 以绝对位置方式移动(防止相对位置丢失精度)
            adx, ady = abs(mx), abs(my)
            if adx <= ady:
                # 水平方向移动的距离短
                for i in range(1, adx):
                    ix = i if mx > 0 else -i
                    temp = int(ady / adx * abs(ix))
                    iy = temp if my > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)
            else:
                # 垂直方向移动的距离短
                for i in range(1, ady):
                    iy = i if my > 0 else -i
                    temp = int(adx / ady * abs(iy))
                    ix = temp if mx > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)

    @staticmethod
    def down(code):
        if ok:
            driver.mouse_down(code)

    @staticmethod
    def up(code):
        if ok:
            driver.mouse_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
        :return:
        """
        if ok:
            driver.mouse_down(code)
            driver.mouse_up(code)


class Keyboard:

    @staticmethod
    def press(code):
        if ok:
            driver.key_down(code)

    @staticmethod
    def release(code):
        if ok:
            driver.key_up(code)

    @staticmethod
    def click(code):
        """
        键盘按键函数中,传入的参数采用的是键盘按键对应的键码
        :param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
        :return:
        """
        if ok:
            driver.key_down(code)
            driver.key_up(code)


class Monitor:
    """
    显示器
    """
    sct = mss.mss()

    @staticmethod
    def grab(region):
        """
        region: tuple, (left, top, width, height)
        pip install mss
        """
        left, top, width, height = region
        return Monitor.sct.grab(monitor={
    
    'left': left, 'top': top, 'width': width, 'height': height})

    @staticmethod
    def pixel(x, y):
        """
        效率很低且不稳定, 单点检测都要耗时1-10ms
        获取颜色, COLORREF 格式, 0x00FFFFFF
        结果是int,
        可以通过 print(hex(color)) 查看十六进制值
        可以通过 print(color == 0x00FFFFFF) 进行颜色判断
        """
        hdc = user32.GetDC(None)
        return gdi32.GetPixel(hdc, x, y)

    class Resolution:
        """
        分辨率
        """

        @staticmethod
        def display():
            """
            显示分辨率
            """
            w = user32.GetSystemMetrics(0)
            h = user32.GetSystemMetrics(1)
            return w, h

        @staticmethod
        def virtual():
            """
            多屏幕组合的虚拟显示器分辨率
            """
            w = user32.GetSystemMetrics(78)
            h = user32.GetSystemMetrics(79)
            return w, h

        @staticmethod
        def physical():
            """
            物理分辨率
            """
            hdc = user32.GetDC(None)
            w = gdi32.GetDeviceCaps(hdc, 118)
            h = gdi32.GetDeviceCaps(hdc, 117)
            return w, h


class Game:
    """
    游戏工具
    """

    @staticmethod
    def game():
        """
        是否在游戏内(顶层窗口是游戏,且正在进行一局游戏,且游戏界面上有血条)
        """
        # 先判断是否是游戏窗口
        hwnd = user32.GetForegroundWindow()
        length = user32.GetWindowTextLengthW(hwnd)
        buffer = ctypes.create_unicode_buffer(length + 1)
        user32.GetWindowTextW(hwnd, buffer, length + 1)
        if 'Apex Legends' != buffer.value:
            return False
        # 是在游戏中, 再判断下是否有血条和生存物品包
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.game)
        for item in data:
            x, y = item.get(cfg.point)
            if Monitor.pixel(x, y) != item.get(cfg.color):
                return False
        return True

    @staticmethod
    def index():
        """
        武器索引和子弹类型索引
        :return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
                 子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.pack)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        if data.get(cfg.color) == color:
            return None, None
        else:
            bullet = data.get(hex(color))
            return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)

    @staticmethod
    def weapon(index, bullet):
        """
        通过武器位和子弹类型识别武器, 参考:config.detect.name
        :param index: 武器位, 1:1号位, 2:2号位
        :param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
        :return:
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.name)
        color = data.get(cfg.color)
        if index == 1:
            lst = data.get(str(index)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x, y):
                    return i + 1
        elif index == 2:
            differ = data.get(str(index)).get(cfg.differ)
            lst = data.get(str(1)).get(str(bullet))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x + differ, y):
                    return i + 1
        return None

    @staticmethod
    def mode():
        """
        武器模式
        :return:  1:全自动, 2:半自动, None:其他
        """
        w, h = Monitor.Resolution.display()
        data = detect.get(f'{
      
      w}:{
      
      h}').get(cfg.mode)
        color = data.get(cfg.color)
        x, y = data.get('1')
        if color == Monitor.pixel(x, y):
            return 1
        x, y = data.get('2')
        if color == Monitor.pixel(x, y):
            return 2
        return None

    @staticmethod
    def detect(data):
        """
        决策是否需要压枪, 向信号量写数据
        """
        if data[cfg.switch] is False:
            print('开关已关闭')
            return
        t1 = time.perf_counter_ns()
        if Game.game() is False:
            print('不在游戏中')
            data[cfg.shake] = None
            return
        index, bullet = Game.index()
        if (index is None) | (bullet is None):
            print('没有武器')
            data[cfg.shake] = None
            return
        if Game.mode() is None:
            print('不是自动/半自动武器')
            data[cfg.shake] = None
            return
        arms = Game.weapon(index, bullet)
        if arms is None:
            print('识别武器失败')
            data[cfg.shake] = None
            return
        # 检测通过, 需要压枪
        gun = weapon.get(str(bullet)).get(str(arms))
        data[cfg.shake] = gun.get(cfg.shake)  # 记录当前武器抖动参数
        t2 = time.perf_counter_ns()
        print(f'耗时:{
      
      t2-t1}ns, 约{
      
      (t2-t1)//1000000}ms, {
      
      gun.get(cfg.name)}')

apex.py

import multiprocessing
import time
from multiprocessing import Process

import pynput  # conda install pynput

import toolkit


end = 'end'
fire = 'fire'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
strength = 'strength'
init = {
    
    
    end: False,  # 退出标记, End 键按下后改为 True, 其他进程线程在感知到变更后结束自身
    switch: True,  # 开关
    fire: False,  # 开火状态
    shake: None,  # 抖枪参数
}


def listener(data):

    def down(x, y, button, pressed):
        if data[end]:
            return False  # 结束监听线程
        if button == pynput.mouse.Button.right:
            if pressed:
                toolkit.Game.detect(data)
        elif button == pynput.mouse.Button.left:
            data[fire] = pressed

    mouse = pynput.mouse.Listener(on_click=down)
    mouse.start()

    def release(key):
        if key == pynput.keyboard.Key.end:
            # 结束程序
            data[end] = True
            return False
        elif key == pynput.keyboard.Key.home:
            # 压枪开关
            data[switch] = not data[switch]
        elif key == pynput.keyboard.Key.esc:
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.Key.tab:
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('1'):
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('2'):
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('3'):
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('e'):
            toolkit.Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('v'):
            toolkit.Game.detect(data)

    keyboard = pynput.keyboard.Listener(on_release=release)
    keyboard.start()
    keyboard.join()  # 卡住监听进程, 当键盘线程结束后, 监听进程才能结束


def suppress(data):
    while True:
        if data[end]:
            break
        if data[switch] is False:
            continue
        if data[fire] & (data[shake] is not None):
            # 301 大约75ms一发子弹
            total = 0  # 总计时 ms
            delay = 1  # 延迟 ms
            pixel = 4  # 抖动像素
            while True:
                if not data[fire]:
                    break
                # 下压
                t = time.perf_counter_ns()
                if total < data[shake][speed] * data[shake][count]:
                    toolkit.Mouse.move(0, data[shake][strength])
                    time.sleep(delay / 1000)
                    total += delay
                else:
                    toolkit.Mouse.move(0, 1)
                    time.sleep(delay / 1000)
                    total += delay
                # 抖枪
                toolkit.Mouse.move(pixel, 0)
                time.sleep(delay / 1000)
                total += delay
                toolkit.Mouse.move(-pixel, 0)
                time.sleep(delay / 1000)
                total += delay
                total += (time.perf_counter_ns() - t) // 1000 // 1000


if __name__ == '__main__':
    multiprocessing.freeze_support()  # windows 平台使用 multiprocessing 必须在 main 中第一行写这个
    manager = multiprocessing.Manager()
    data = manager.dict()  # 创建进程安全的共享变量
    data.update(init)  # 将初始数据导入到共享变量
    # 将键鼠监听和压枪放到单独进程中跑
    p1 = Process(target=listener, args=(data,))  # 监听进程
    p2 = Process(target=suppress, args=(data,))  # 压枪进程
    p1.start()
    p2.start()
    p1.join()  # 卡住主进程, 当进程 listener 结束后, 主进程才会结束

第三阶段实现 放弃抖枪术 转常规后座抵消法 (优化中)

调了几把枪, 有无涡轮, 有无双发扳机, 有压枪参数的使用压枪参数, 其他的使用抖枪参数, 压枪与抖枪并存

我的游戏内鼠标设置是这样的, 要确保每个瞄镜的ADS都是一样的, 鼠标DPI是3200

最终的效果是, 20米很稳, 30米将就, 50米不太行, 有几率一梭子打倒, 再往后就没意义了. 差不多够用了, 就没再认真调
在这里插入图片描述

如何调压枪参数

我觉得调参数最重要的一点, 就是先算出正确的子弹射速(平均每发子弹耗时), 如果用了错误的数据, 那很可能调了半天白费功夫

测试方法我总结了下, 首先, 每发子弹耗时通常都是50到150毫秒, 先假设是100, 看有多少发子弹, 就复制多少条压枪数据, 举例

R-301 这把枪, 加上金扩容, 28发子弹, 那就先准备下面的初始数据, 三个参数分别是, 鼠标水平移动的值/垂直移动的值/移动后休眠时间, 当然也可以有其他的参数

先把对应最后一发子弹的鼠标移动值设置为10000, 看是否打完子弹时, 鼠标正好产生大幅位移, 然后调后面的100, 直到恰好匹配, 然后就可以开始调鼠标参数了

[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],  #
[0, 0, 100],
[0, 0, 100],
[10000, 0, 100],

调鼠标参数时, 要从上往下逐个调, 因为上面的一个变动, 对下面的影响非常大, 很可能导致下面的白调了

比如调纵向压制的时候, 1倍镜30米瞄这这道杠打, 争取基本全都在杠上, 纵向就ok了, 横向同理

在这里插入图片描述

也可以借助录像工具, 录制屏幕中心部分区域, 然后以0.1倍速播放, 仔细查看压制力度是否合适

最终的效果就是, 不太稳定, 123倍镜表现不太一致, 3倍镜偏差最大. 难不成各个镜子做一套参数?

游戏中实测

整体来说, 表现将就吧, 平行还行, 其他一般, 距离一条线还远得很

存在的问题

  • 采用取色判断法, 单点取色耗时1-10ms, 性能不足
  • 检测武器名称使用的是O(n)时间复杂度的遍历方式, 在取色判断法效率低的情况下, 性能不够优秀和稳定, 期望做到O(1)
  • 暂无法判断是否持有武器(有武器但我用拳头, 可能引起错误地触发压枪)
  • 运行一会儿就会卡顿, 非常稳定,平均两秒卡一下,鼠标键盘电脑画面全部都同时卡一下, 严重影响游戏, 不知道到底什么原因
  • 暂无法实现按着左键时模拟左键点击效果, 所以暂无法实现单发枪变连发枪的功能

详细代码

cfg.py

mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
armed = 'armed'
empty = 'empty'
switch = 'switch'
bullet = 'bullet'  # 子弹
differ = 'differ'
turbo = 'turbo'
trigger = 'trigger'
restrain = 'restrain'
strength = 'strength'
positive = 'positive'  # 肯定的
negative = 'negative'  # 否定的

# 检测数据
detect = {
    
    
    "3440:1440": {
    
    
        game: [  # 判断是否在游戏中
            {
    
    
                point: (236, 1344),  # 点的坐标, 血条左上角
                color: 0x00FFFFFF  # 点的颜色, 255, 255, 255
            },
            {
    
    
                point: (2692, 1372),  # 生存物品右下角
                color: 0x959595  # 149, 149, 149
            }
        ],
        pack: {
    
      # 背包状态, 有无武器, 选择的武器
            point: (2900, 1372),  # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
            color: 0x808080,  # 无武器时, 灰色, 128, 128, 128
            '0x447bb4': 1,  # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
            '0x839b54': 2,  # 重型弹药武器
            '0x3da084': 3,  # 能量弹药武器
            '0xce5f6e': 4,  # 狙击弹药武器
            '0xf339b': 5,  # 霰弹枪弹药武器
            '0x5302ff': 6,  # 空投武器
        },
        mode: {
    
      # 武器模式, 全自动/半自动/单发/其他
            color: 0x00FFFFFF,
            '1': (3151, 1347),  # 全自动
            '2': (3171, 1351),  # 半自动
        },
        armed: {
    
      # 是否持有武器(比如有武器但用拳头就是未持有武器)

        },
        empty: {
    
      # 是否空弹夹(武器里子弹数为0)
            color: 0x00FFFFFF,
            '1': (3204, 1306),  # 十位数, 该点白色即非0, 非0则一定不空
            '2': (3229, 1294),  # 个位数, 该点白色即为0, 十位为0且个位为0为空
        },
        name: {
    
      # 武器名称判断
            color: 0x00FFFFFF,
            '1': {
    
      # 1号武器
                '1': [  # 轻型弹药武器
                    (2959, 1386),  # 1: RE-45 自动手枪
                    (2970, 1385),  # 2: 转换者冲锋枪
                    (2972, 1386),  # 3: R-301 卡宾枪
                    (2976, 1386),  # 4: R-99 冲锋枪
                    (2980, 1386),  # 5: P2020 手枪
                    (2980, 1384),  # 6: 喷火轻机枪
                    (2987, 1387),  # 7: G7 侦查枪
                    (3015, 1386),  # 8: CAR (轻型弹药)
                ],
                '2': [  # 重型弹药武器
                    (2957, 1385),  # 1: 赫姆洛克突击步枪
                    (2982, 1385),  # 2: 猎兽冲锋枪
                    (2990, 1393),  # 3: 平行步枪
                    (3004, 1386),  # 4: 30-30
                    (3015, 1386),  # 5: CAR (重型弹药)
                ],
                '3': [  # 能量弹药武器
                    (2955, 1386),  # 1: L-STAR 能量机枪
                    (2970, 1384),  # 2: 三重式狙击枪
                    (2981, 1385),  # 3: 电能冲锋枪
                    (2986, 1384),  # 4: 专注轻机枪
                    (2980, 1384),  # 5: 哈沃克步枪
                ],
                '4': [  # 狙击弹药武器
                    (2969, 1395),  # 1: 哨兵狙击步枪
                    (2999, 1382),  # 2: 充能步枪
                    (2992, 1385),  # 3: 辅助手枪
                    (3016, 1383),  # 4: 长弓
                ],
                '5': [  # 霰弹枪弹药武器
                    (2957, 1384),  # 1: 和平捍卫者霰弹枪
                    (2995, 1382),  # 2: 莫桑比克
                    (3005, 1386),  # 3: EVA-8
                ],
                '6': [  # 空投武器
                    (2958, 1384),  # 1: 克雷贝尔狙击枪
                    (2959, 1384),  # 2: 手感卓越的刀刃
                    (2983, 1384),  # 3: 敖犬霰弹枪
                    (3003, 1383),  # 4: 波塞克
                    (3014, 1383),  # 5: 暴走
                ]
            },
            '2': {
    
    
                differ: 195  # 直接用1的坐标, 横坐标右移195就可以了
            }
        },
        turbo: {
    
      # 涡轮
            color: 0x00FFFFFF,
            '3': {
    
    
                differ: 2,  # 有涡轮和没涡轮的索引偏移
                '4': (3072, 1358),  # 专注轻机枪 涡轮检测位置
                '5': (3034, 1358),  # 哈沃克步枪 涡轮检测位置
            }
        },
        trigger: {
    
      # 双发扳机
            color: 0x00FFFFFF,
            '1': {
    
    
                differ: 2,
                '7': (3072, 1358),  # G7 侦查枪 双发扳机检测位置
            },
            '5': {
    
    
                differ: 1,
                '3': (3034, 1358),  # EVA-8 双发扳机检测位置
            }
        }
    },
    "2560:1440": {
    
    

    },
    "2560:1080": {
    
    

    },
    "1920:1080": {
    
    

    }
}

# 武器数据
weapon = {
    
    
    '1': {
    
      # 轻型弹药武器
        '1': {
    
    
            name: 'RE-45 自动手枪',  # 全程往右飘
            shake: {
    
    
                speed: 80,
                count: 10,
                strength: 5,
            },
            restrain: [
                [1, -2, 10, 80],  #
                [1, -2, 10, 80],
                [1, -2, 10, 80],
                [1, -4, 10, 80],
                [1, -6, 10, 80],
                [1, -7, 8, 80],  #
                [1, -7, 8, 80],
                [1, -7, 8, 80],
                [1, -7, 8, 80],
                [1, -7, 8, 80],
                [1, -1, 5, 80],  #
                [1, -1, 5, 80],
                [1, -1, 5, 80],
                [1, -1, 5, 80],
                [1, -1, 5, 80],
                [1, -1, 5, 80],  #
                [1, -1, 3, 80],
                [1, -1, 3, 80],
                [1, -1, 3, 80],
                [1, -1, 3, 80],
                [1, -1, 3, 80],  #
                [1, -2, 3, 80],
                [1, -2, 3, 80],
                [1, -2, 3, 80],
                [1, -2, 3, 80],
                [1, -5, 3, 80],  #
                [1, -5, 3, 80],
                [1, -10, 3, 80],
                [1, -10, 3, 80],
                [1, -10, 3, 80],
            ]
        },
        '2': {
    
    
            name: '转换者冲锋枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 7,
            },
            restrain: [
                [1, 0, 15, 94],
                [1, 0, 15, 94],
                [1, 0, 15, 94],
                [1, 0, 15, 94],
                [1, 0, 15, 94],  #
                [1, 0, 15, 94],
                [1, 0, 15, 94],
                [1, 0, 10, 94],
                [1, 0, 10, 94],
                [1, 0, 10, 94],  #
                [1, -5, 5, 94],
                [1, -5, 5, 94],
                [1, -5, 5, 94],
                [1, 0, 5, 94],
                [1, 0, 5, 94],  #
                [1, 0, 5, 94],
                [1, 5, 5, 94],
                [1, 5, 5, 94],
                [1, 5, 5, 94],
                [1, 0, 5, 94],  #
                [1, 0, 5, 94],
                [1, 0, 5, 94],
                [1, 0, 5, 94],
                [1, 0, 5, 94],
                [1, 0, 5, 94],  #
                [1, 0, 5, 94],
                [1, 0, 0, 94],
            ]
        },
        '3': {
    
    
            name: 'R-301 卡宾枪',
            shake: {
    
    
                speed: 64,  # 74ms打一发子弹
                count: 6,  # 压制前6发
                strength: 5,  # 压制的力度(下移的像素)
            },
            restrain: [
                [1, -5, 10, 70],
                [1, 0, 10, 70],
                [1, -5, 10, 70],
                [1, -2, 10, 70],
                [1, 0, 10, 70],  #
                [1, 0, 5, 70],
                [1, 0, 0, 70],
                [1, -5, 0, 70],
                [1, -5, 5, 70],
                [1, 0, 0, 70],  #
                [1, 0, 0, 70],
                [1, 5, 10, 70],
                [1, 5, 5, 70],
                [1, 5, 0, 70],
                [1, 5, 0, 70],  #
                [1, 0, 0, 70],
                [1, 5, 0, 70],
                [1, 5, 10, 70],
                [1, 0, 10, 70],
                [1, -5, 0, 70],  #
                [1, -5, 0, 70],
                [1, -5, 0, 70],
                [1, -5, 0, 70],
                [1, -5, 0, 70],
                [1, 0, 0, 70],  #
                [1, 0, 0, 70],
                [1, 0, 0, 70],
                [1, 0, 0, 64],
            ]
        },
        '4': {
    
    
            name: 'R-99 冲锋枪',
            shake: {
    
    
                speed: 55.5,
                count: 13,
                strength: 8,
            },
            restrain: [
                [1, 0, 10, 48],
                [1, 0, 10, 48],
                [1, 0, 10, 48],
                [1, -5, 10, 48],
                [1, -5, 10, 48],  #
                [1, -5, 10, 48],
                [1, -5, 10, 48],
                [1, 0, 10, 48],
                [1, 0, 10, 48],
                [1, 0, 10, 48],  #
                [1, 5, 10, 48],
                [1, 5, 10, 48],
                [1, 5, 10, 48],
                [1, 0, 10, 48],
                [1, 0, 0, 48],  #
                [1, -5, 0, 48],
                [1, -10, 0, 48],
                [1, 0, 0, 48],
                [1, 0, 0, 48],
                [1, 5, 5, 48],  #
                [1, 10, 5, 48],
                [1, 10, 5, 48],
                [1, 5, 0, 48],
                [1, 0, 0, 48],
                [1, -5, 0, 48],  #
                [1, -5, 0, 48],
                [1, -5, 0, 48],
            ]
        },
        '5': {
    
    
            name: 'P2020 手枪',
            restrain: [
                [2, 1, 100],
            ]
        },
        '6': {
    
    
            name: '喷火轻机枪',
            shake: {
    
    
                speed: 110,
                count: 8,
                strength: 5,
            },
            restrain: [
                [1, 0, 20, 100],
                [1, 5, 15, 100],
                [1, 5, 15, 100],
                [1, 5, 15, 100],
                [1, 5, 15, 100],  #
                [1, 5, 15, 100],
                [1, -5, 10, 100],
                [1, -5, 0, 100],
                [1, -5, 0, 100],
                [1, -5, 0, 100],  #
                [1, 0, 0, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 5, 5, 100],
                [1, 10, 5, 100],  #
                [1, 10, 5, 100],
                [1, 5, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],  # 20
                [1, 0, 0, 100],
                [1, 0, 0, 100],
                [1, 0, 0, 100],
                [1, 0, 0, 100],
                [1, -5, 5, 100],  #
                [1, -5, 5, 100],
                [1, -5, 5, 100],
                [1, -5, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],  #
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],  #
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],  #
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],  #
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 5, 100],
                [1, 0, 0, 100],  #
            ]
        },
        '7': {
    
    
            name: 'G7 侦查枪',
        },
        '8': {
    
    
            name: 'CAR (轻型弹药)',
            shake: {
    
    
                speed: 64.5,
                count: 10,
                strength: 7,
            },
            restrain: [
                [1, 0, 10, 58],  #
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],  #
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, -5, 10, 58],
                [1, -5, 10, 58],
                [1, -5, 5, 58],  #
                [1, -5, 10, 58],
                [1, -5, 0, 58],
                [1, 0, 0, 58],
                [1, 5, 0, 58],
                [1, 5, 3, 58],  #
                [1, 5, 3, 58],
                [1, -5, 3, 58],
                [1, -5, 3, 58],
                [1, -5, 3, 58],
                [1, 0, 0, 58],  #
                [1, 0, 0, 58],
                [1, 0, 0, 58],
                [1, 0, 3, 58],
                [1, 0, 3, 58],
                [1, 0, 3, 58],  #
                [1, 0, 3, 58],
            ]
        },
        '9': {
    
    
            name: 'G7 侦查枪 (双发扳机)',
            restrain: [
                [1, 0, 5, 20]
            ]
        },
    },
    '2': {
    
      # 重型弹药武器
        '1': {
    
    
            name: '赫姆洛克突击步枪',
            shake: {
    
    
                speed: 50,
                count: 3,
                strength: 6,
            }
        },
        '2': {
    
    
            name: '猎兽冲锋枪',
            shake: {
    
    
                speed: 50,
                count: 5,
                strength: 6,
            }
        },
        '3': {
    
    
            name: '平行步枪',
            shake: {
    
    
                speed: 100,
                count: 5,
                strength: 5,
            },
            restrain: [
                [1, 0, 10, 100],  #
                [1, 5, 10, 100],
                [1, 5, 10, 100],
                [1, 5, 10, 100],
                [1, 5, 10, 100],
                [1, -5, 10, 100],  #
                [1, -5, 0, 100],
                [1, -5, 0, 100],
                [1, -5, 0, 100],
                [1, 0, 5, 100],
                [1, 5, 5, 100],  #
                [1, 5, 5, 100],
                [1, 5, 0, 100],
                [1, 5, 0, 100],
                [1, 0, 0, 100],
                [1, 5, 5, 100],  #
                [1, 5, 5, 100],
                [1, 5, 5, 100],
                [1, 0, 0, 100],
                [1, 0, 0, 100],
                [1, -5, 5, 100],  #
                [1, -5, 5, 100],
                [1, -5, 5, 100],
                [1, -0, 5, 100],
                [1, 5, 5, 100],
                [1, 5, 5, 100],  #
                [1, 5, 5, 100],
                [1, -5, -5, 100],
                [1, -5, 5, 100],
                [1, -5, 5, 100],
            ]
        },
        '4': {
    
    
            name: '30-30',
        },
        '5': {
    
    
            name: 'CAR (重型弹药)',
            shake: {
    
    
                speed: 58,
                count: 10,
                strength: 7,
            },
            restrain: [
                [1, 0, 10, 58],  #
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, 3, 10, 58],  #
                [1, 3, 10, 58],
                [1, 3, 10, 58],
                [1, -5, 10, 58],
                [1, -5, 10, 58],
                [1, -5, 5, 58],  #
                [1, -5, 10, 58],
                [1, -5, 0, 58],
                [1, 0, 0, 58],
                [1, 5, 0, 58],
                [1, 5, 3, 58],  #
                [1, 5, 3, 58],
                [1, -5, 3, 58],
                [1, -5, 3, 58],
                [1, -5, 3, 58],
                [1, 0, 0, 58],  #
                [1, 0, 0, 58],
                [1, 0, 0, 58],
                [1, 0, 3, 58],
                [1, 0, 3, 58],
                [1, 0, 3, 58],  #
                [1, 0, 3, 58],
            ]
        }
    },
    '3': {
    
      # 能量弹药武器
        '1': {
    
    
            name: 'L-STAR 能量机枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 5,
            }
        },
        '2': {
    
    
            name: '三重式狙击枪',
        },
        '3': {
    
    
            name: '电能冲锋枪',
            shake: {
    
    
                speed: 83.3,
                count: 10,
                strength: 7,
            },
            restrain: [
                [1, -5, 15, 80],
                [1, 0, 15, 80],
                [1, 0, 15, 80],
                [1, 0, 15, 80],
                [1, 0, 15, 80],  #
                [1, -5, 10, 80],
                [1, -5, 10, 80],
                [1, -5, 10, 80],
                [1, 0, 10, 80],
                [1, 5, 10, 80],  #
                [1, 5, 5, 80],
                [1, 5, 5, 80],
                [1, 5, 5, 80],
                [1, 0, 5, 80],
                [1, 0, 5, 80],  #
                [1, 0, 5, 80],
                [1, 0, 0, 80],
                [1, 0, 0, 80],
                [1, 0, 0, 80],
                [1, 0, 0, 80],  #
                [1, 0, 0, 80],
                [1, 5, 0, 80],
                [1, 5, 0, 80],
                [1, 5, 0, 80],
                [1, 0, 0, 80],  #
                [1, 0, 0, 80],
            ]
        },
        '4': {
    
    
            name: '专注轻机枪',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 7,
            }
        },
        '5': {
    
    
            name: '哈沃克步枪',
            shake: {
    
    
                speed: 100,
                count: 8,
                strength: 6,
            },
            restrain: [
                [1, 0, 0, 400],  # 延迟
                [1, -5, 10, 88],  # 1
                [1, -5, 15, 88],
                [1, 0, 15, 88],
                [1, 0, 15, 88],
                [1, 0, 15, 88],
                [1, 5, 10, 88],  #
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, -5, 5, 88],
                [1, -5, 0, 88],  # 1
                [1, -5, 0, 88],
                [1, -10, 0, 88],
                [1, -10, 0, 88],
                [1, -5, 0, 88],
                [1, 0, 5, 88],  #
                [1, 10, 5, 88],
                [1, 10, 5, 88],
                [1, 0, 0, 88],
                [1, 0, 0, 88],
                [1, 5, 10, 88],  # 1
                [1, 5, 10, 88],
                [1, 0, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],  #
                [1, 5, 5, 88],
                [1, 5, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 0, 88],
                [1, 0, 0, 88],  # 1
                [1, 0, 0, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],  #
            ]
        },
        '6': {
    
    
            name: '专注轻机枪 (涡轮)',
            shake: {
    
    
                speed: 100,
                count: 10,
                strength: 7,
            }
        },
        '7': {
    
    
            name: '哈沃克步枪 (涡轮)',
            shake: {
    
    
                speed: 100,
                count: 8,
                strength: 6,
            },
            restrain: [
                [1, -5, 10, 88],  # 1
                [1, -5, 15, 88],
                [1, 0, 15, 88],
                [1, 0, 15, 88],
                [1, 0, 15, 88],
                [1, 5, 10, 88],  #
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, -5, 5, 88],
                [1, -5, 0, 88],  # 1
                [1, -5, 0, 88],
                [1, -10, 0, 88],
                [1, -10, 0, 88],
                [1, -5, 0, 88],
                [1, 0, 5, 88],  #
                [1, 10, 5, 88],
                [1, 10, 5, 88],
                [1, 0, 0, 88],
                [1, 0, 0, 88],
                [1, 5, 10, 88],  # 1
                [1, 5, 10, 88],
                [1, 0, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],
                [1, 5, 10, 88],  #
                [1, 5, 5, 88],
                [1, 5, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 0, 88],
                [1, 0, 0, 88],  # 1
                [1, 0, 0, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],
                [1, 0, 5, 88],  #
            ]
        },
    },
    '4': {
    
      # 狙击弹药武器
        '1': {
    
    
            name: '哨兵狙击步枪',
        },
        '2': {
    
    
            name: '充能步枪',
        },
        '3': {
    
    
            name: '辅助手枪',
        },
        '4': {
    
    
            name: '长弓',
        },
    },
    '5': {
    
      # 霰弹弹药武器
        '1': {
    
    
            name: '和平捍卫者霰弹枪',
        },
        '2': {
    
    
            name: '莫桑比克',
        },
        '3': {
    
    
            name: 'EVA-8',
        },
        '4': {
    
    
            name: 'EVA-8 (双发扳机)',
        }
    },
    '6': {
    
      # 空投武器
        '1': {
    
    
            name: '克雷贝尔狙击枪',
        },
        '2': {
    
    
            name: '手感卓越的刀刃',
        },
        '3': {
    
    
            name: '敖犬霰弹枪',
        },
        '4': {
    
    
            name: '波塞克',
        },
        '5': {
    
    
            name: '暴走',
            shake: {
    
    
                speed: 200,
                count: 8,
                strength: 2,
            }
        },
    }
}

toolkit.py

import time

import mss  # pip install mss
import ctypes

from ctypes import CDLL

import cfg
from cfg import detect, weapon

# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32

try:
    driver = CDLL(r'mouse.device.lgs.dll')  # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
    ok = driver.device_open() == 1
    if not ok:
        print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
    print('初始化失败, 缺少文件')


class Mouse:

    @staticmethod
    def point():
        return user32.GetCursorPos()

    @staticmethod
    def move(x, y, absolute=False):
        if ok:
            if (x == 0) & (y == 0):
                return
            mx, my = x, y
            if absolute:
                ox, oy = user32.GetCursorPos()
                mx = x - ox
                my = y - oy
            driver.moveR(mx, my, True)

    @staticmethod
    def moveHumanoid(x, y, absolute=False):
        """
        仿真移动(还没做好)
        """
        if ok:
            ox, oy = user32.GetCursorPos()  # 原鼠标位置
            mx, my = x, y  # 相对移动距离
            if absolute:
                mx = x - ox
                my = y - oy
            tx, ty = ox + mx, oy + my
            print(f'({
      
      ox},{
      
      oy}), ({
      
      tx},{
      
      ty}), x:{
      
      mx},y:{
      
      my}')
            # 以绝对位置方式移动(防止相对位置丢失精度)
            adx, ady = abs(mx), abs(my)
            if adx <= ady:
                # 水平方向移动的距离短
                for i in range(1, adx):
                    ix = i if mx > 0 else -i
                    temp = int(ady / adx * abs(ix))
                    iy = temp if my > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)
            else:
                # 垂直方向移动的距离短
                for i in range(1, ady):
                    iy = i if my > 0 else -i
                    temp = int(adx / ady * abs(iy))
                    ix = temp if mx > 0 else -temp
                    Mouse.move(ox + ix, oy + iy, absolute=True)
                    # time.sleep(0.001)

    @staticmethod
    def down(code):
        if ok:
            driver.mouse_down(code)

    @staticmethod
    def up(code):
        if ok:
            driver.mouse_up(code)

    @staticmethod
    def click(code):
        """
        :param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
        :return:
        """
        if ok:
            driver.mouse_down(code)
            driver.mouse_up(code)


class Keyboard:

    @staticmethod
    def press(code):
        if ok:
            driver.key_down(code)

    @staticmethod
    def release(code):
        if ok:
            driver.key_up(code)

    @staticmethod
    def click(code):
        """
        键盘按键函数中,传入的参数采用的是键盘按键对应的键码
        :param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
        :return:
        """
        if ok:
            driver.key_down(code)
            driver.key_up(code)


class Monitor:
    """
    显示器
    """
    sct = mss.mss()

    @staticmethod
    def grab(region):
        """
        region: tuple, (left, top, width, height)
        pip install mss
        """
        left, top, width, height = region
        return Monitor.sct.grab(monitor={
    
    'left': left, 'top': top, 'width': width, 'height': height})

    @staticmethod
    def pixel(x, y):
        """
        效率很低且不稳定, 单点检测都要耗时1-10ms
        获取颜色, COLORREF 格式, 0x00FFFFFF
        结果是int,
        可以通过 print(hex(color)) 查看十六进制值
        可以通过 print(color == 0x00FFFFFF) 进行颜色判断
        """
        hdc = user32.GetDC(None)
        return gdi32.GetPixel(hdc, x, y)

    class Resolution:
        """
        分辨率
        """

        @staticmethod
        def display():
            """
            显示分辨率
            """
            w = user32.GetSystemMetrics(0)
            h = user32.GetSystemMetrics(1)
            return w, h

        @staticmethod
        def virtual():
            """
            多屏幕组合的虚拟显示器分辨率
            """
            w = user32.GetSystemMetrics(78)
            h = user32.GetSystemMetrics(79)
            return w, h

        @staticmethod
        def physical():
            """
            物理分辨率
            """
            hdc = user32.GetDC(None)
            w = gdi32.GetDeviceCaps(hdc, 118)
            h = gdi32.GetDeviceCaps(hdc, 117)
            return w, h


class Game:
    """
    游戏工具
    """

    @staticmethod
    def key():
        w, h = Monitor.Resolution.display()
        return f'{
      
      w}:{
      
      h}'

    @staticmethod
    def game():
        """
        是否游戏窗体在最前
        """
        # 先判断是否是游戏窗口
        hwnd = user32.GetForegroundWindow()
        length = user32.GetWindowTextLengthW(hwnd)
        buffer = ctypes.create_unicode_buffer(length + 1)
        user32.GetWindowTextW(hwnd, buffer, length + 1)
        if 'Apex Legends' != buffer.value:
            return False
        return True

    @staticmethod
    def play():
        """
        是否正在玩
        """
        # 是在游戏中, 再判断下是否有血条和生存物品包
        data = detect.get(Game.key()).get(cfg.game)
        for item in data:
            x, y = item.get(cfg.point)
            if Monitor.pixel(x, y) != item.get(cfg.color):
                return False
        return True

    @staticmethod
    def index():
        """
        武器索引和子弹类型索引
        :return: 武器位索引, 1:1号位, 2:2号位, None:无武器
                 子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
        """
        data = detect.get(Game.key()).get(cfg.pack)
        x, y = data.get(cfg.point)
        color = Monitor.pixel(x, y)
        if data.get(cfg.color) == color:
            return None, None
        else:
            bi = data.get(hex(color))
            return (1, bi) if color == Monitor.pixel(x, y + 1) else (2, bi)

    @staticmethod
    def weapon(pi, bi):
        """
        通过武器位和子弹类型识别武器, 参考:config.detect.name
        :param pi: 武器位, 1:1号位, 2:2号位
        :param bi: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
        :return:
        """
        data = detect.get(Game.key()).get(cfg.name)
        color = data.get(cfg.color)
        if pi == 1:
            lst = data.get(str(pi)).get(str(bi))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x, y):
                    return i + 1
        elif pi == 2:
            differ = data.get(str(pi)).get(cfg.differ)
            lst = data.get(str(1)).get(str(bi))
            for i in range(len(lst)):
                x, y = lst[i]
                if color == Monitor.pixel(x + differ, y):
                    return i + 1
        return None

    @staticmethod
    def mode():
        """
        武器模式
        :return:  1:全自动, 2:半自动, None:其他
        """
        data = detect.get(Game.key()).get(cfg.mode)
        color = data.get(cfg.color)
        x, y = data.get('1')
        if color == Monitor.pixel(x, y):
            return 1
        x, y = data.get('2')
        if color == Monitor.pixel(x, y):
            return 2
        return None

    @staticmethod
    def armed():
        """
        是否持有武器
        """
        return True

    @staticmethod
    def empty():
        """
        是否空弹夹
        """
        data = detect.get(Game.key()).get(cfg.empty)
        color = data.get(cfg.color)
        x, y = data.get('1')
        if color == Monitor.pixel(x, y):
            return False
        x, y = data.get('2')
        return color == Monitor.pixel(x, y)

    @staticmethod
    def turbo(bi, wi):
        """
        判断是否有涡轮, 只有配置了检测涡轮的武器才会做取色判断
        :return: (False, None), (True, differ), 有涡轮的话, 额外返回涡轮索引偏移
        """
        data = detect.get(Game.key()).get(cfg.turbo)
        color = data.get(cfg.color)
        data = data.get(str(bi))
        if data is None:
            return False, None
        differ = data.get(cfg.differ)
        data = data.get(str(wi))
        if data is None:
            return False, None
        x, y = data
        result = color == Monitor.pixel(x, y)
        return (True, differ) if result else (False, None)

    @staticmethod
    def trigger(bi, wi):
        """
        判断是否有双发扳机, 只有配置了检测双发扳机的武器才会做取色判断
        :return: (False, None), (True, differ), 有双发扳机的话, 额外返回双发扳机索引偏移
        """
        data = detect.get(Game.key()).get(cfg.trigger)
        color = data.get(cfg.color)
        data = data.get(str(bi))
        if data is None:
            return False, None
        differ = data.get(cfg.differ)
        data = data.get(str(wi))
        if data is None:
            return False, None
        x, y = data
        result = color == Monitor.pixel(x, y)
        return (True, differ) if result else (False, None)

    @staticmethod
    def detect(data):
        """
        决策是否需要压枪, 向信号量写数据
        """
        t1 = time.perf_counter_ns()
        if data.get(cfg.switch) is False:
            t2 = time.perf_counter_ns()
            print(f'耗时: {
      
      t2 - t1}ns, 约{
      
      (t2 - t1) // 1000000}ms, 开关已关闭')
            return
        if Game.game() is False:
            data[cfg.shake] = None
            data[cfg.restrain] = None
            t2 = time.perf_counter_ns()
            print(f'耗时: {
      
      t2 - t1}ns, 约{
      
      (t2 - t1) // 1000000}ms, 不在游戏中')
            return
        if Game.play() is False:
            data[cfg.shake] = None
            data[cfg.restrain] = None
            t2 = time.perf_counter_ns()
            print(f'耗时: {
      
      t2 - t1}ns, 约{
      
      (t2 - t1) // 1000000}ms, 不在游戏中')
            return
        pi, bi = Game.index()
        if (pi is None) | (bi is None):
            data[cfg.shake] = None
            data[cfg.restrain] = None
            t2 = time.perf_counter_ns()
            print(f'耗时: {
      
      t2 - t1}ns, 约{
      
      (t2 - t1) // 1000000}ms, 没有武器')
            return
        # if Game.mode() is None:
        #     data[cfg.shake] = None
        #     data[cfg.restrain] = None
        #     t2 = time.perf_counter_ns()
        #     print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 不是自动/半自动武器')
        #     return
        wi = Game.weapon(pi, bi)
        if wi is None:
            data[cfg.shake] = None
            data[cfg.restrain] = None
            t2 = time.perf_counter_ns()
            print(f'耗时: {
      
      t2 - t1}ns, 约{
      
      (t2 - t1) // 1000000}ms, 识别武器失败')
            return
        # 检测通过, 需要压枪
        # 检测涡轮
        result, differ = Game.turbo(bi, wi)
        if result is False:
            # 检测双发扳机
            result, differ = Game.trigger(bi, wi)
        # 拿对应参数
        gun = weapon.get(str(bi)).get(str((wi + differ) if result else wi))
        data[cfg.shake] = gun.get(cfg.shake)  # 记录当前武器抖动参数
        data[cfg.restrain] = gun.get(cfg.restrain)  # 记录当前武器压制参数
        t2 = time.perf_counter_ns()
        print(f'耗时: {
      
      t2-t1}ns, 约{
      
      (t2-t1)//1000000}ms, {
      
      gun.get(cfg.name)}')

apex.py

import multiprocessing
import time
from multiprocessing import Process

import pynput  # conda install pynput

from toolkit import Mouse, Game

end = 'end'
fire = 'fire'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
restart = 'restart'
restrain = 'restrain'
strength = 'strength'
init = {
    
    
    end: False,  # 退出标记, End 键按下后改为 True, 其他进程线程在感知到变更后结束自身
    switch: True,  # 检测和压枪开关
    fire: False,  # 开火状态
    shake: None,  # 抖枪参数
    restrain: None,  # 压枪参数
}


def listener(data):

    def down(x, y, button, pressed):
        if data.get(end):
            return False  # 结束监听线程
        if button == pynput.mouse.Button.right:
            if pressed:
                Game.detect(data)
        elif button == pynput.mouse.Button.left:
            data[fire] = pressed

    mouse = pynput.mouse.Listener(on_click=down)
    mouse.start()

    def release(key):
        if key == pynput.keyboard.Key.end:
            # 结束程序
            data[end] = True
            return False
        elif key == pynput.keyboard.Key.home:
            # 压枪开关
            data[switch] = not data.get(switch)
        elif key == pynput.keyboard.Key.esc:
            Game.detect(data)
        elif key == pynput.keyboard.Key.tab:
            Game.detect(data)
        elif key == pynput.keyboard.Key.alt_l:
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('1'):
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('2'):
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('3'):
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('e'):
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('r'):
            Game.detect(data)
        elif key == pynput.keyboard.KeyCode.from_char('v'):
            Game.detect(data)

    keyboard = pynput.keyboard.Listener(on_release=release)
    keyboard.start()
    keyboard.join()  # 卡住监听进程, 当键盘线程结束后, 监听进程才能结束


def suppress(data):
    while True:
        if data.get(end):
            break
        if data.get(switch) is False:
            continue
        if data.get(fire):
            if data.get(restrain) is not None:
                for item in data.get(restrain):
                    if not data.get(fire):  # 停止开火
                        break
                    t1 = time.perf_counter_ns()
                    if not Game.game():  # 不在游戏中
                        break
                    if not Game.armed():  # 未持有武器
                        break
                    if Game.empty():  # 弹夹为空
                        break
                    t2 = time.perf_counter_ns()
                    # operation: # 1:移动 2:按下
                    operation = item[0]
                    if operation == 1:
                        temp, x, y, delay = item
                        Mouse.move(x, y)
                        time.sleep((delay - (t2 - t1) // 1000 // 1000) / 1000)
                    elif operation == 2:
                        temp, code, delay = item
                        Mouse.click(code)
                        time.sleep((delay - (t2 - t1) // 1000 // 1000) / 1000)
            elif data.get(shake) is not None:
                total = 0  # 总计时 ms
                delay = 1  # 延迟 ms
                pixel = 4  # 抖动像素
                while True:
                    if not data[fire]:  # 停止开火
                        break
                    if not Game.game():  # 不在游戏中
                        break
                    if not Game.armed():  # 未持有武器
                        break
                    if Game.empty():  # 弹夹为空
                        break
                    t = time.perf_counter_ns()
                    if total < data[shake][speed] * data[shake][count]:
                        Mouse.move(0, data[shake][strength])
                        time.sleep(delay / 1000)
                        total += delay
                    else:
                        Mouse.move(0, 1)
                        time.sleep(delay / 1000)
                        total += delay
                    # 抖枪
                    Mouse.move(pixel, 0)
                    time.sleep(delay / 1000)
                    total += delay
                    Mouse.move(-pixel, 0)
                    time.sleep(delay / 1000)
                    total += delay
                    total += (time.perf_counter_ns() - t) // 1000 // 1000


if __name__ == '__main__':
    multiprocessing.freeze_support()  # windows 平台使用 multiprocessing 必须在 main 中第一行写这个
    manager = multiprocessing.Manager()
    data = manager.dict()  # 创建进程安全的共享变量
    data.update(init)  # 将初始数据导入到共享变量
    # 将键鼠监听和压枪放到单独进程中跑
    p1 = Process(daemon=True, target=listener, args=(data,))  # 监听进程
    p2 = Process(target=suppress, args=(data,))  # 压枪进程
    p1.start()
    p2.start()
    p1.join()  # 卡住主进程, 当进程 listener 结束后, 主进程才会结束

打包与使用

第四阶段实现 AI 目标检测, 移动鼠标, 彻底告别压枪

Python Apex Legends AI 自瞄 全过程记录

猜你喜欢

转载自blog.csdn.net/mrathena/article/details/126918389
今日推荐