Python Apex Legends *器武**自动识别与压枪

文章目录

环境准备

操纵键鼠

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

toolkit.py

游戏内测试

键鼠监听

*器武**识别

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

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

如何简单且高效判断*器武***弹子**类别

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

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

何时触发识别

压枪思路

组织数据

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

cfg.py

toolkit.py

apex.py

第二阶段实现 能自动识别出所有*器武**并采用对应压枪参数执行压枪

第三阶段实现 放弃抖枪术 转常规后座抵消法

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

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

环境准备

Python Windows 开发环境搭建

说明

基础环境

从 python 官网*载下**安装包并安装, 配置环境变量后, 在命令行内可以执行 python 命令

包管理工具

  • pip: 一个现代的,通用的 Python 包管理工具。提供了对 Python 包的查找、*载下**、安装、卸载的功能。注:pip 已内置于 Python 3.4 和 2.7 及以上版本,其他版本需另行安装。

虚拟环境

python 基础环境下, 不同的依赖只能存在一个版本, 而不同的项目可能依赖了同一个包的不同版本, 这样的项目就可能无法在同一个 python 基础环境下运行. 基于基础环境创建的虚拟环境是相互隔离的, 第三方依赖包可根据项目要求自行*载下**, 不同项目运行在不同的虚拟环境几下就可以避免以来冲突等问题

虚拟环境只能基于本地存在的基础环境来创建, 会继承基础环境自带的库, 可以选择是否继承基础环境的已安装的第三方包

我觉得可以借鉴学习 java maven 的依赖管理理念, 告别虚拟环境

虚拟环境管理工具

  • virtualenv: virtualenv可以为每个项目创建一套隔离的Python环境, 再使用pip进行包管理
  • venv: python 3.3 起自带的虚拟环境管理工具
  • pipenv:
  • virtualenvwrapper:
  • virtualenvwrapper-win:
  • virtualenv-burrito:
  • autoenv:
  • pyvenv:
  • pyenv:

Conda

CondaCondaMinicondaAnaconda

Conda 是一个开源的 环境和包管理系统, 它可以创建并管理完全隔离的不同版本的 python 环境, 也可以创建并管理某 python 版本的完全隔离的虚拟环境, 用了它就不必再安装基础环境了

默认配置下, Conda 可以安装和管理由 Anaconda® 构建、审查和维护的数千个包。版本通常低于最新版

  • Anaconda: Anaconda是一个打包的集合,里面预装好了 Conda、Python、众多数据科学和机器学习相关的包、科学计算工具等等,所以也称为Python的一种发行版。
  • Miniconda: Miniconda 是一个免费的 conda 最小安装程序。 它是 Anaconda 的一个小型引导版本,仅包含 Conda、Python、它们所依赖的包以及少量其他有用的包,包括 pip、zlib 和其他一些包。
  • Anaconda Navigator: Anaconda 的 GUI 管理工具

基础环境搭建

Python 官网Python Windows *载下**

到官网找到 Windows 最新版*载下**并安装

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

pip 是 Python 包管理工具,该工具提供了对Python 包的查找、*载下**、安装、卸载的功能。

什么是 Python Launcher?

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

python 安装程序会自动在 path 环境变量中添加这两条目录

PythonApexLegends*器武**自动识别与压枪

目录结构说明

vc dll 结构体_python的安装目录结构

PythonApexLegends*器武**自动识别与压枪

  • python*ex.e**: python 解释器, 运行时会弹出控制台窗口
  • pythonw*ex.e**: 无窗口的python可执行程序, 代码在后台运行
  • DLLs: 包含 python 的 *.pyd(Python动态模块)文件与几个Windows的 *.dll(动态链接库)文件pyd 文件是由 D 语言编写的一种 dll 文件, 可以保护 python 文件的源码不被暴露
  • Doc: 帮助文档
  • include: python 的 C 语言接口头文件(.h结尾), 当在 C 程序中集成 python 时, 会用到这个目录下的头文件C语言中, 后缀为 .h 的文件是头文件, 内含函数声明、宏定义、结构体定义等内容。 后缀为 .c 的文件是源文件, 内含函数实现,变量定义等内容。 为什么要有头文件? C/C++编译的时候先扫描整个文件有没有语法错误, 然后将C语句转化为汇编, 当碰到不认识的变量、类、函数、对象的命名时, 首先查找它有没有声明, 如果没有声明直接报错, 如果有,则根据对应的定义空出一定的存储空间并进行相关的指令转化。
  • Lib: python 自带的标准库/包/测试套件等
  • Lib/site-packages: 存放安装的第三方库, pip install 安装的第三方库就放在这里
  • libs: python 的 C 语言接口库文件
  • Scripts: 脚本文件, 如 pip*ex.e** 包管理器等
  • tcl: python 与 TCL 的结合
  • Tools: 一些工具

Miniconda 环境搭建

Miniconda

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

红字提示, 不推荐勾选添加环境变量, 因为可能会导致因路径被添加到靠前的位置而造成问题. 如果是首次安装 python 相关环境, 可以选择添加到环境变量选项, 如果已经有在用的其他配置了 PATH 的 Conda 或者 Python 则不建议

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

确实在用户环境变量 PATH 里加了很多目录, 查看这些目录下都有哪些 exe, 根目录下有 python*ex.e**

PythonApexLegends*器武**自动识别与压枪

使用方式

安装完成后, 从开始菜单中找到并打开 [Anaconda Prompt], 运行 [conda list] 命令, 如果正确安装, 则会出现已安装的包列表

PythonApexLegends*器武**自动识别与压枪

常用命令

Command referenceconda常用命令:安装,更新,创建,激活,关闭,查看,卸载,删除,清理,重命名,换源,问题Anaconda /Miniconda 常用命令CONDA集合

查看帮助

conda -h
conda --help
conda install -h
conda install --help
conda env -h
12345

查看信息

conda info # 包含 conda, python, pip 等, 还有当前在 conda 命令行中激活的环境
1

PythonApexLegends*器武**自动识别与压枪

列出环境

conda env list
conda info -e
12

PythonApexLegends*器武**自动识别与压枪

新安装的 Conda 只有 base 基础环境, 没有虚拟环境

配置源

windows环境下conda更换为国内清华镜像源

编辑用户目录下的 .condarc 文件即可更换 conda 默认源。

# Windows 用户无法直接创建名为 .condarc 的文件,需要先执行如下命令,生成该文件后再修改。C:\Users\用户名\.condarc
# 设置搜索时显示通道地址
conda config --set show_channel_urls yes
123

修改文件内容

channels:
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/menpo/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/bioconda/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/msys2/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/
  - https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/pytorch/
  - defaults
show_channel_urls: true
12345678910

运行 conda clean -i 清除索引缓存,保证用的是镜像站提供的索引。

运行 conda config --show 确认源信息

虚拟环境

# 创建虚拟环境
conda create -h
conda create -n testenv
conda create -n testenv2 python=3.8
conda create -n testenv3 python=3.10.7 # 貌似不能下 Anaconda 库中没有的 python 版本, 表现就是转圈很久
conda create -p C:\mrathena\develop\workspace\pycharm\yolov5-6.2\venv
# 查看环境包
conda list # 查看当前激活环境的包, 默认激活的是 base 基础环境
conda list -n testenv # 查看指定虚拟环境的包
# 激活虚拟环境
conda activate testenv
conda activate C:\mrathena\develop\workspace\pycharm\yolov5-6.2\venv
# 反激活
conda deactivate # 退出虚拟环境, 重新激活 base 基础环境
# 删除虚拟环境
conda remove -n testenv --all
conda remove -p C:\mrathena\develop\workspace\pycharm\yolov5-6.2\venv --all
1234567891011121314151617

如果报错如下, 检查是否有开代理工具, 关闭代理, 重开工具就可以了

CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/win-64/current_repodata.json>
1

创建虚拟环境的注意点

创建虚拟环境时, 一定要指定一个不同于 base python 版本的 python 版本

不然的话, 新的虚拟环境基本等同于没有创建, 使用的仍然是 base 环境, 执行 pip install 会污染 base 环境, 真是恶心

创建了一个不同于 base python 版本的虚拟环境后, 在虚拟环境中会实打实包含类似 base 的目录结构, 也包含对应的 pip*ex.e**, 这时候再执行 pip install 就不会影响到 base 环境了

IDE PyCharm

pycharm的virtualenv、pipenv、conda详解

*载下**最新版如 pycharm-professional-2021.2.3*ex.e**

以下选项自选

  • Create Desktop Shortcut, 64-bit launcher, 创建64位启动器的桌面快捷方式, 非常建议
  • Update context menu, Add “Open Folder as Project”, 在上下文菜单(文件夹右键)添加"以项目的方式打开该文件夹"选项, 可选
  • Create Associations, .py, 创建 .py 文件的关联, 默认使用 PyCharm 打开 .py 文件, 非常建议
  • Download and install JRE x86 by JetBrains, *载下** JRE? 不确定做什么, 不选
  • Update PATH variable(restart needed), Add “bin” folder to the PATH, 更新 PATH 环境变量, 将启动器目录添加到 PATH, 不选

创建工程时, 建议每个工程都创建新的虚拟环境, 通过 Conda

PythonApexLegends*器武**自动识别与压枪

测试

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

PythonApexLegends*器武**自动识别与压枪

在 conda 命令行中也能看到 pycharm 中创建的虚拟环境, 但是没有名字

插件

Chinese (Simplified) Language Pack / 中文语言包

寻找文档

Pypi

在官网输入包名, 找到包, 点进去, 里面一般都会有项目说明, GitHub, 文档等内容

conda create -n apex python=3.9
1

操纵键鼠

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

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

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

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

LGS_9.02.65_x64_Logitech*ex.e**, 网盘*载下**其他地址1其他地址2

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

装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 没有对应的文档, 只能猜测参数了

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)

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970

游戏内测试

在游戏里面试过后, 管用, 但是不准, 猜测可能和游戏内鼠标灵敏度/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()
123456789101112

键鼠监听

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

这里我使用的钩子是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()
1234567891011121314

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

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

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

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

*器武**识别

PythonApexLegends*器武**自动识别与压枪

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

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

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

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

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

PythonApexLegends*器武**自动识别与压枪

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

如何简单且高效判断*器武***弹子**类别

可以和上面的放在一起, 同一个点直接判断出背包状态和*器武***弹子**类别

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

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

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

PythonApexLegends*器武**自动识别与压枪

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

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

收起*器武**, 部分*器武**可以通过[V]标判断, 放弃

何时触发识别

键盘 1/2/3/E/V 释放, 鼠标 右键 按下, 这个如果不影响开枪就这个了, 影响的话就改成侧下键. 键位和键在游戏内的功能不冲突的

压枪思路

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

  • 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
  • 根据*器武**配件等测试不同情况下的*器武**后坐力数据, 然后做反向抵消.可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱

我先试试 抖枪*法大**

组织数据

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

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

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

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

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

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
            }
        }
    },
    "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: '暴走',
        },
    }
}

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206

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)

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277

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()

123456789101112131415161718192021222324252627282930313233343536373839404142434445

PythonApexLegends*器武**自动识别与压枪

第二阶段实现 能自动识别出所有*器武**并采用对应压枪参数执行压枪

第三阶段实现 放弃抖枪术 转常规后座抵消法


1