博文目录

文章目录

  • 环境准备
    • 操纵键鼠
      • 驱动安装 链接库加载 代码准备和游戏外测试
        • toolkit.py
      • 游戏内测试
    • 键鼠监听
  • 武器识别
    • 如何判断是否在游戏内
    • 如何判断背包状态 无武器/1号武器/2号武器
    • 如何判断武器子弹类别 轻型/重型/能量/狙击/霰弹/空投
    • 如何判断武器名称 在确定当前使用的背包的基础上判断
    • 如何判断武器模式 全自动/连发/单发
    • 如何判断是否持有武器
    • 如何判断弹夹是否打空
    • 何时触发识别
    • 几个细节点
    • 压枪思路
    • 组织数据
  • 开发过程
    • 第一阶段实现 能自动识别出所有武器
    • 第二阶段实现 能自动采用对应抖枪参数执行压枪
    • 第三阶段实现 能自动采用对应压枪参数执行压枪
      • 如何调压枪参数
      • 游戏中实测
      • 存在的问题
  • 工程源码
    • cfg.py
    • toolkit.py
    • apex.py
    • 打包与使用
  • 拓展 目标检测 与 自瞄, 彻底告别压枪
  • 拓展 通用型人体骨骼检测 与 自瞄, 训练一次, FPS 游戏通用

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

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

环境准备

Python Windows 开发环境搭建

conda create -n apex python=3.9

操纵键鼠

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

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

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

网盘下载 LGS_9.02.65_x64_Logitech.exe

网盘下载 mouse.device.lgs.dll

try:    driver = ctypes.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('初始化罗技驱动失败, 缺少文件')

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

toolkit.py

import timefrom ctypes import CDLLimport win32api  # conda install pywin32try:    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 Mouseimport pynput  # conda install pynputdef 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 说明

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, 导致鼠标键盘失效

回调方法如果返回 False, 监听线程就会自动结束, 所以不要随便返回 False

键盘的特殊按键采用 keyboard.Key.tab 这种写法,普通按键用 keyboard.KeyCode.from_char('c') 这种写法, 有些键不知道该怎么写, 可以 print(key) 查看信息

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

武器识别

如何判断是否在游戏内

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

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

一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定, 不会受不同背景色的影响

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

如何判断背包状态 无武器/1号武器/2号武器


黄圈内的武器面板, 可以分为两个部分, 上边是边框, 下边是名字, 上边的边框又可以分为上半部分a和下半部分b

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

如何判断武器子弹类别 轻型/重型/能量/狙击/霰弹/空投

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

如何判断武器名称 在确定当前使用的背包的基础上判断

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

先从名字最长的武器开始找点, 从最左边或最右边找, 即可确保该点一定不在别的武器上或在别的武器上该点不是指定的颜色

也可以写一个工具来找点, 原理很简单, 以同样的 left,top,width,height 截取所有武器的名称图, 遍历每一个像素点, 在该点下再遍历所有名称图, 如果只有某张名称图的该点是纯白色, 则记录该点, 可以通过游戏中取该点色, 判断是否纯白, 来唯一确认该武器名称

下面代码是 3440-1440 分辨率下 PUBG 的名称图找点方法, APEX 也类似, 改改 left,top,width,height 就行了

import osimport cv2directory = r'34401440\group'def load(directory):    imgs = []    names = []    for item in os.listdir(directory):        path = os.path.join(directory, item)        if os.path.isdir(path):            t1, t2 = load(path)            for temp in t1:                imgs.append(temp)            for temp in t2:                names.append(temp)        elif os.path.isfile(path):            imgs.append(cv2.imread(path, cv2.IMREAD_COLOR))            names.append(item.replace('.png', ''))    return imgs, namesimgs, names = load(directory)print(f'载入{len(imgs)}张图片')left, top, width, height = 2253, 125, 260, 42exclude = set()data = []# 图片尺寸: 260,42for row in range(0, height):    for col in range(0, width):        # 轮流遍历每张图片的同一个点, 如果该点只有一张图片是纯白色, 则该点为该图片的识别点, 输入记录        # 某点纯白色次数        counter = 0        temp = -1        for i in range(0, len(imgs)):            img = imgs[i]            name = names[i]            if name in exclude:                continue            (b, g, r) = img[row, col]            if (b, g, r) == (255, 255, 255):                counter += 1                temp = i        if counter == 1:            name = names[temp]            print(f'用于验证的数据:{row}, {col} - 截图上的坐标:({col},{row}) - 对应游戏内的点的坐标:({left + col},{top + row}) - {name}')            exclude.add(name)            data.append((left + col, top + row, name))print(f'data = [')for item in data:    print(f'    {item},')print(f']')

如何判断武器模式 全自动/连发/单发


需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(想把单发武器做成自动连发), 喷子和狙不需要压枪

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

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

如何判断是否持有武器

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

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

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

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

如何判断弹夹是否打空


弹夹中子弹数大多为两位数(LSTAR可能为三位数), 所以只需确认十位不为0, 即可认为不空, 十位为0且个位为0, 即可认为空

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

何时触发识别

  • 鼠标右键(瞄准模式) 按下, 识别武器. 和游戏内原本的按键功能不冲突
  • 1(1号武器) / 2(2号武器) / 3(收起武器) / E(交互/换枪) / V(切换射击模式) / R(更换弹夹) / Tab(打开背包) / Esc(关闭各种窗口) / Alt(求生物品) 键释放, 识别武器
  • Home 键释放 / 鼠标侧下键按下, 切换开关
  • end 键释放, 结束程序

几个细节点

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

压枪思路

apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值” />
我的游戏内鼠标设置是这样的, 要确保每个瞄镜的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倍镜偏差最大. 难不成各个镜子做一套参数?

游戏中实测

压枪参数大多都是随便调了下, 并不是很精细, 20米内的表现还行吧, 当然距离一条线还差得远. 专注轻机枪因为射速不固定所以没调

存在的问题

  • 采用取色判断法, 单点取色耗时1-10ms, 性能不足 (已有优化思路, 一种是, 通过GetCurrentObject和GetObject获取和hdc相关的位图对象数据区起始地址, 拿到BitMap, 直接取对应坐标的颜色. 受限于个人水平, 暂无法用Python实现)
  • 检测武器名称使用的是O(n)时间复杂度的遍历方式, 在取色判断法效率低的情况下, 性能不够优秀和稳定, 期望做到O(1)
  • 暂无法判断是否持有武器(有武器但我用拳头, 可能引起错误地触发压枪)
  • 暂无法实现按着左键时模拟左键点击效果, 所以暂无法实现单发枪变连发枪的功能

工程源码

也可以直接拷贝下方代码. 因条件限制, 只适配了 3440*1440 分辨率的游戏数据, 其他分辨率需自行适配

游戏偶尔会调整武器射速, 发现数据不对劲时, 可能需要重新调整压枪参数

GitHub python.apex.weapon.auto.recognize.and.suppress

网盘下载 LGS_9.02.65_x64_Logitech.exe

网盘下载 mouse.device.lgs.dll

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, 64],                [1, -2, 10, 64],                [1, -2, 10, 64],                [1, -4, 10, 64],                [1, -6, 10, 64],  #                [1, -5, 8, 64],                [1, -5, 8, 64],                [1, -5, 8, 64],                [1, -5, 8, 64],                [1, -5, 8, 64],  #                [1, 0, 5, 64],                [1, 0, 5, 64],                [1, -5, 5, 64],                [1, -5, 5, 64],                [1, -5, 5, 64],  #                [1, -5, 3, 64],                [1, 0, 3, 64],                [1, 0, 3, 64],                [1, 0, 3, 64],                [1, 0, 3, 64],  #                [1, -5, 3, 64],                [1, -5, 3, 64],                [1, -5, 3, 64],                [1, -5, 3, 64],                [1, -5, 3, 64],  #            ]        },        '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, 10, 100],  #                [1, 5, 10, 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],                [1, 0, 1, 0]            ]        },    },    '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,            },            restrain: [                [1, 12, 10, 100],                [1, 12, 10, 100],                [1, 10, 10, 100],                [1, 0, 10, 100],                [1, -10, 10, 100],  #                [1, -10, 10, 100],                [1, -10, 10, 100],                [1, 0, 10, 100],                [1, 0, 10, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],  #                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],  #                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],  #                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],                [1, 0, 8, 100],  #            ]        },        '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 ctypesimport timefrom win32api import GetSystemMetricsfrom win32con import SM_CXSCREEN, SM_CYSCREEN, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, DESKTOPHORZRES, DESKTOPVERTRESfrom win32print import GetDeviceCapsfrom win32gui import GetCursorPos, GetDC, ReleaseDC, GetPixel, GetWindowText, GetForegroundWindow  # conda install pywin32,import cfgfrom cfg import detect, weapontry:    driver = ctypes.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:            if x == 0 and y == 0:                return            mx, my = x, y            if absolute:                ox, oy = 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)class Monitor:    """    显示器    """    @staticmethod    def pixel(x, y):        """        效率很低且不稳定, 单点检测都要耗时1-10ms        获取颜色, COLORREF 格式, 0x00FFFFFF        结果是int,        可以通过 print(hex(color)) 查看十六进制值        可以通过 print(color == 0x00FFFFFF) 进行颜色判断        """        hdc = GetDC(None)        color = GetPixel(hdc, x, y)        ReleaseDC(None, hdc)  # 一定要释放DC, 不然随着该函数调用次数增加会越来越卡, 表现就是不调用该函数, 系统会每两秒卡一下, 调用次数越多, 卡的程度越厉害        return color    @staticmethod    def resolution():        """        显示分辨率        """        w = GetSystemMetrics(SM_CXSCREEN)        h = GetSystemMetrics(SM_CYSCREEN)        return w, hclass Game:    """    游戏工具    """    @staticmethod    def key():        w, h = Monitor.resolution()        return f'{w}:{h}'    @staticmethod    def game():        """        是否游戏窗体在最前        """        return 'Apex Legends' == GetWindowText(GetForegroundWindow())    @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 multiprocessingimport timefrom multiprocessing import Processimport pynput  # pip install pynputfrom toolkit import Mouse, Gameend = '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 mouse(data):    def down(x, y, button, pressed):        if button == pynput.mouse.Button.right:            if pressed:                Game.detect(data)        elif button == pynput.mouse.Button.left:            data[fire] = pressed        elif button == pynput.mouse.Button.x1:            if pressed:                data[switch] = not data.get(switch)    with pynput.mouse.Listener(on_click=down) as m:        m.join()def keyboard(data):    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)    with pynput.keyboard.Listener(on_release=release) as k:        k.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)                        delay = (delay - (t2 - t1) // 1000 // 1000) / 1000                        if delay > 0:                            time.sleep(delay)                    elif operation == 2:                        temp, code, delay = item                        Mouse.click(code)                        delay = (delay - (t2 - t1) // 1000 // 1000) / 1000                        if delay > 0:                            time.sleep(delay)            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 // 1000if __name__ == '__main__':    multiprocessing.freeze_support()  # windows 平台使用 multiprocessing 必须在 main 中第一行写这个    manager = multiprocessing.Manager()    data = manager.dict()  # 创建进程安全的共享变量    data.update(init)  # 将初始数据导入到共享变量    # 将键鼠监听和压枪放到单独进程中跑    pm = Process(target=mouse, args=(data,))    pk = Process(target=keyboard, args=(data,))    ps = Process(target=suppress, args=(data,))    pm.start()    pk.start()    ps.start()    pk.join()  # 不写 join 的话, 使用 dict 的地方就会报错 conn = self._tls.connection, AttributeError: 'ForkAwareLocal' object has no attribute 'connection'    pm.terminate()  # 鼠标进程无法主动监听到终止信号, 所以需强制结束

打包与使用

Anaconda Prompt (miniconda) 中先激活对应的虚拟环境, 然后切到工程目录, 执行

pip install pyinstaller
# 主要文件是 apex.py, 将以 dist/apex 作为输出目录pyinstaller apex.py -p cfg.py -p toolkit.py -p mouse.device.lgs.dll

在工程下会多出来一个 dist/apex 目录, 里面就是打包好的程序和相关依赖了, 将整个文件夹打包成压缩包即可分享(暂未在其他电脑上验证)

(base) C:\Users\mrathena>conda activate apex(apex) C:\Users\mrathena>pip install pyinstaller(apex) C:\Users\mrathena>cd C:\mrathena\develop\workspace\pycharm\python.apex.helper(apex) C:\mrathena\develop\workspace\pycharm\python.apex.helper>pyinstaller apex.py -p cfg.py -p toolkit.py -p mouse.device.lgs.dll889 INFO: PyInstaller: 5.4.1889 INFO: Python: 3.9.13 (conda)899 INFO: Platform: Windows-10-10.0.22621-SP0900 INFO: wrote C:\mrathena\develop\workspace\pycharm\python.apex.helper\apex.spec903 INFO: UPX is not available.905 INFO: Extending PYTHONPATH with paths['C:\\mrathena\\develop\\workspace\\pycharm\\python.apex.helper', 'C:\\mrathena\\develop\\workspace\\pycharm\\python.apex.helper\\cfg.py', 'C:\\mrathena\\develop\\workspace\\pycharm\\python.apex.helper\\toolkit.py', 'C:\\mrathena\\develop\\workspace\\pycharm\\python.apex.helper\\mouse.device.lgs.dll']1624 INFO: checking Analysis1624 INFO: Building Analysis because Analysis-00.toc is non existent1625 INFO: Initializing module dependency graph...1629 INFO: Caching module graph hooks...1640 WARNING: Several hooks defined for module 'numpy'. Please take care they do not conflict.1646 INFO: Analyzing base_library.zip ...5586 INFO: Loading module hook 'hook-encodings.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...7785 INFO: Loading module hook 'hook-pickle.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...8553 INFO: Loading module hook 'hook-heapq.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...9233 INFO: Caching module dependency graph...9449 INFO: running Analysis Analysis-00.toc9469 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable  required by C:\mrathena\develop\miniconda\envs\apex\python.exe9704 INFO: Analyzing C:\mrathena\develop\workspace\pycharm\python.apex.helper\apex.py9767 INFO: Loading module hook 'hook-multiprocessing.util.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...9924 INFO: Loading module hook 'hook-xml.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...10394 INFO: Loading module hook 'hook-pynput.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\_pyinstaller_hooks_contrib\\hooks\\stdhooks'...11733 INFO: Processing pre-safe import module hook six.moves from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks\\pre_safe_import_module\\hook-six.moves.py'.11889 INFO: Loading module hook 'hook-platform.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks'...12003 INFO: Processing module hooks...12085 INFO: Loading module hook 'hook-Xlib.py' from 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\_pyinstaller_hooks_contrib\\hooks\\stdhooks'...14018 INFO: Looking for ctypes DLLs14043 INFO: Analyzing run-time hooks ...14046 INFO: Including run-time hook 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_subprocess.py'14048 INFO: Including run-time hook 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py'14050 INFO: Including run-time hook 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py'14054 INFO: Including run-time hook 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py'14061 INFO: Looking for dynamic libraries917 INFO: Extra DLL search directories (AddDllDirectory): []917 INFO: Extra DLL search directories (PATH): ['C:\\mrathena\\develop\\miniconda\\envs\\apex', 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\Library\\mingw-w64\\bin', 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\Library\\usr\\bin', 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\Library\\bin', 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\Scripts', 'C:\\mrathena\\develop\\miniconda\\envs\\apex\\bin', 'C:\\mrathena\\develop\\miniconda\\condabin', 'C:\\WINDOWS\\system32', 'C:\\WINDOWS', 'C:\\WINDOWS\\System32\\Wbem', 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0', 'C:\\WINDOWS\\System32\\OpenSSH', 'C:\\Program Files\\dotnet', 'C:\\mrathena\\develop\\xftp', 'C:\\mrathena\\develop\\tortoise.git\\bin', 'C:\\Program Files (x86)\\Common Files\\Thunder Network\\KanKan\\Codecs', 'C:\\Program Files\\NVIDIA Corporation\\NVIDIA NvDLISR', 'C:\\Program Files (x86)\\NVIDIA Corporation\\PhysX\\Common', 'C:\\mrathena\\develop\\xshell', 'C:\\mrathena\\application\\mpv.player', '.', 'C:\\WINDOWS\\system32', 'C:\\WINDOWS', 'C:\\WINDOWS\\System32\\Wbem', 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0', 'C:\\WINDOWS\\System32\\OpenSSH', 'C:\\mrathena\\develop\\miniconda', 'C:\\mrathena\\develop\\miniconda\\Library\\mingw-w64\\bin', 'C:\\mrathena\\develop\\miniconda\\Library\\usr\\bin', 'C:\\mrathena\\develop\\miniconda\\Library\\bin', 'C:\\mrathena\\develop\\miniconda\\Scripts', 'C:\\mrathena\\develop\\python-3.10.7\\Scripts', 'C:\\mrathena\\develop\\python-3.10.7', 'C:\\Users\\mrathena\\AppData\\Local\\Microsoft\\WindowsApps', 'C:\\mrathena\\application\\bandizip', 'C:\\mrathena\\develop\\jdk-17.0.3.1\\bin', 'C:\\mrathena\\develop\\apache-maven-3.6.3\\bin', 'C:\\mrathena\\develop\\portable.git\\bin', 'C:\\Users\\mrathena\\.dotnet\\tools', 'C:\\mrathena\\develop\\fiddler', 'C:\\mrathena\\application\\mpv.player', 'C:\\mrathena\\develop\\vapour-synth-r59', 'C:\\mrathena\\develop\\vapour-synth-r59\\core', 'C:\\mrathena\\develop\\vapour-synth-r59\\vsrepo', 'C:\\Users\\mrathena\\.dotnet\\tools', 'C:\\Users\\mrathena\\AppData\\Local\\Microsoft\\WindowsApps', '.']15680 INFO: Looking for eggs15680 INFO: Using Python library C:\mrathena\develop\miniconda\envs\apex\python39.dll15681 INFO: Found binding redirects:[]15708 INFO: Warnings written to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\warn-apex.txt15754 INFO: Graph cross-reference written to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\xref-apex.html15782 INFO: checking PYZ15782 INFO: Building PYZ because PYZ-00.toc is non existent15783 INFO: Building PYZ (ZlibArchive) C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\PYZ-00.pyz16420 INFO: Building PYZ (ZlibArchive) C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\PYZ-00.pyz completed successfully.16433 INFO: checking PKG16433 INFO: Building PKG because PKG-00.toc is non existent16433 INFO: Building PKG (CArchive) apex.pkg16446 INFO: Building PKG (CArchive) apex.pkg completed successfully.16448 INFO: Bootloader C:\mrathena\develop\miniconda\envs\apex\lib\site-packages\PyInstaller\bootloader\Windows-64bit\run.exe16448 INFO: checking EXE16448 INFO: Building EXE because EXE-00.toc is non existent16449 INFO: Building EXE from EXE-00.toc16450 INFO: Copying bootloader EXE to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\apex.exe.notanexecutable16489 INFO: Copying icon to EXE16489 INFO: Copying icons from ['C:\\mrathena\\develop\\miniconda\\envs\\apex\\lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-console.ico']16491 INFO: Writing RT_GROUP_ICON 0 resource with 104 bytes16491 INFO: Writing RT_ICON 1 resource with 3752 bytes16491 INFO: Writing RT_ICON 2 resource with 2216 bytes16491 INFO: Writing RT_ICON 3 resource with 1384 bytes16492 INFO: Writing RT_ICON 4 resource with 37019 bytes16493 INFO: Writing RT_ICON 5 resource with 9640 bytes16493 INFO: Writing RT_ICON 6 resource with 4264 bytes16494 INFO: Writing RT_ICON 7 resource with 1128 bytes16530 INFO: Copying 0 resources to EXE16531 INFO: Embedding manifest in EXE16532 INFO: Updating manifest in C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\apex.exe.notanexecutable16534 INFO: Updating resource type 24 name 1 language 016570 INFO: Appending PKG archive to EXE16573 INFO: Fixing EXE headers16732 INFO: Building EXE from EXE-00.toc completed successfully.16735 INFO: checking COLLECT16735 INFO: Building COLLECT because COLLECT-00.toc is non existent16736 INFO: Building COLLECT COLLECT-00.toc17544 INFO: Building COLLECT COLLECT-00.toc completed successfully.(apex) C:\mrathena\develop\workspace\pycharm\python.apex.helper>

拓展 目标检测 与 自瞄, 彻底告别压枪

Python Apex Legends YOLO v5 AI 自瞄 全过程记录

不同的游戏, 都需要准备大量数据集做训练, 才能取得比较好的效果

拓展 通用型人体骨骼检测 与 自瞄, 训练一次, FPS 游戏通用

【亦】警惕AI外挂!我写了一个枪枪爆头的视觉AI,又亲手“杀死”了它

YOLO V7 keypoint 人体关键点检测

大多数 FPS 游戏中要检测的目标都为人形, 可以训练一个 通用型人体骨骼检测模型, 在各种游戏中都能起到不错的效果