Python Apex Legends 武器自动识别与压枪 全过程记录
博文目录
文章目录
- 环境准备
- 操纵键鼠
- 驱动安装 链接库加载 代码准备和游戏外测试
- toolkit.py
- 游戏内测试
- 键鼠监听
- 武器识别
- 如何判断是否在游戏内
- 如何判断背包状态 无武器/1号武器/2号武器
- 如何判断武器子弹类别 轻型/重型/能量/狙击/霰弹/空投
- 如何判断武器名称 在确定当前使用的背包的基础上判断
- 如何判断武器模式 全自动/连发/单发
- 如何判断是否持有武器
- 如何判断弹夹是否打空
- 何时触发识别
- 几个细节点
- 压枪思路
- 组织数据
- 开发过程
- 第一阶段实现 能自动识别出所有武器
- 第二阶段实现 能自动采用对应抖枪参数执行压枪
- 第三阶段实现 能自动采用对应压枪参数执行压枪
- 如何调压枪参数
- 游戏中实测
- 存在的问题
- 工程源码
- cfg.py
- toolkit.py
- apex.py
- 打包与使用
- 拓展 AI 目标检测, 移动鼠标, 彻底告别压枪
本文为下面参考文章的学习与实践
[原文] 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 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 说明
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号位), 通过 武器子弹类别 缩小判断范围, 在每个武器的名字上找一个纯白色的点, 确保这个点只有这把武器是纯白色, 然后逐个对比, 判断当前持有的武器和哪个点对上了, 那就说明是哪把武器
先从名字最长的武器开始找点, 从最左边或最右边找, 即可确保该点一定不在别的武器上或在别的武器上该点不是指定的颜色
如何判断武器模式 全自动/连发/单发
需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(想把单发武器做成自动连发), 喷子和狙不需要压枪
所以需要找一个能区分三种模式的点(不同模式这个点的颜色不同但是稳定), 且这个点不能受和平和三重的特殊标记影响
一开始找了个不是纯白色的点, 后来发现这个点的颜色会被背景颜色影响到, 不是恒定不变的. 最终还是放弃了一个点即可分辨模式的想法, 采用了稳妥的纯白色点, 保证该点只在该模式下是纯白色的, 在其他模式都不是纯白色即可
如何判断是否持有武器
暂无法判断, 收起武器和持有武器, 没有能明确分辨两种情况的固定点
部分武器可以通过[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 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??
- 左右抖动抵消水平后坐力, 下拉抵消垂直后坐力. 这种方法简单, 但是画面会抖动, 效果也不是很好
- 根据武器配件等测试不同情况下的武器后坐力数据, 然后做反向抵消.
- 可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦. 这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱
- 还有就是现在很火的AI目标检测(yolov5), 我也有尝试做, 环境搭好了, 但是中途卡住了
- 一是毕竟python是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊
- 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …
组织数据
- 武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 抖枪参数, 压枪参数等信息
- 配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息将配置分类
- 信号数据, 程序运行时, 进程线程间通讯
具体看 cfg.py 中的 detect, weapon 和 apex.py 中的 init
开发过程
第一阶段实现 能自动识别出所有武器
找对点就行了
第二阶段实现 能自动采用对应抖枪参数执行压枪
- 游戏内鼠标灵敏度越高, 越容易抖枪且效果更好, 但是开到5的话, 会感到有点晕
- 游戏内鼠标灵敏度越高, 代码里抖动的像素就需要设置的更小, 比如5的灵敏度, 抖动2像素就可以了
- 抖枪能减小后坐力, 但不能完全消除, 所以还需配合对应方向的移动
- 后坐力越大的武器, 前几枪容易跳太高, 下压力度可以大点
能量武器, 专注和哈沃克, 预热和涡轮有很大影响, 这里暂时没管, 在第三阶段实现
第三阶段实现 能自动采用对应压枪参数执行压枪
我的游戏内鼠标设置是这样的, 要确保每个瞄镜的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.legends.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 ctypes
import time
from win32api import GetSystemMetrics
from win32con import SM_CXSCREEN, SM_CYSCREEN, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, DESKTOPHORZRES, DESKTOPVERTRES
from win32print import GetDeviceCaps
from win32gui import GetCursorPos, GetDC, ReleaseDC, GetPixel, GetWindowText, GetForegroundWindow # conda install pywin32,
import cfg
from cfg import detect, weapon
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('初始化罗技驱动失败, 缺少文件')
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
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = GetSystemMetrics(SM_CXSCREEN)
h = GetSystemMetrics(SM_CYSCREEN)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = GetSystemMetrics(SM_CXVIRTUALSCREEN)
h = GetSystemMetrics(SM_CYVIRTUALSCREEN)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
hdc = GetDC(None)
w = GetDeviceCaps(hdc, DESKTOPHORZRES)
h = GetDeviceCaps(hdc, DESKTOPVERTRES)
ReleaseDC(None, hdc)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def key():
w, h = Monitor.Resolution.display()
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 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 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 // 1000
if __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.dll
889 INFO: PyInstaller: 5.4.1
889 INFO: Python: 3.9.13 (conda)
899 INFO: Platform: Windows-10-10.0.22621-SP0
900 INFO: wrote C:\mrathena\develop\workspace\pycharm\python.apex.helper\apex.spec
903 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 Analysis
1624 INFO: Building Analysis because Analysis-00.toc is non existent
1625 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.toc
9469 INFO: Adding Microsoft.Windows.Common-Controls to dependent assemblies of final executable
required by C:\mrathena\develop\miniconda\envs\apex\python.exe
9704 INFO: Analyzing C:\mrathena\develop\workspace\pycharm\python.apex.helper\apex.py
9767 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 DLLs
14043 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 libraries
917 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 eggs
15680 INFO: Using Python library C:\mrathena\develop\miniconda\envs\apex\python39.dll
15681 INFO: Found binding redirects:
[]
15708 INFO: Warnings written to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\warn-apex.txt
15754 INFO: Graph cross-reference written to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\xref-apex.html
15782 INFO: checking PYZ
15782 INFO: Building PYZ because PYZ-00.toc is non existent
15783 INFO: Building PYZ (ZlibArchive) C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\PYZ-00.pyz
16420 INFO: Building PYZ (ZlibArchive) C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\PYZ-00.pyz completed successfully.
16433 INFO: checking PKG
16433 INFO: Building PKG because PKG-00.toc is non existent
16433 INFO: Building PKG (CArchive) apex.pkg
16446 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.exe
16448 INFO: checking EXE
16448 INFO: Building EXE because EXE-00.toc is non existent
16449 INFO: Building EXE from EXE-00.toc
16450 INFO: Copying bootloader EXE to C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\apex.exe.notanexecutable
16489 INFO: Copying icon to EXE
16489 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 bytes
16491 INFO: Writing RT_ICON 1 resource with 3752 bytes
16491 INFO: Writing RT_ICON 2 resource with 2216 bytes
16491 INFO: Writing RT_ICON 3 resource with 1384 bytes
16492 INFO: Writing RT_ICON 4 resource with 37019 bytes
16493 INFO: Writing RT_ICON 5 resource with 9640 bytes
16493 INFO: Writing RT_ICON 6 resource with 4264 bytes
16494 INFO: Writing RT_ICON 7 resource with 1128 bytes
16530 INFO: Copying 0 resources to EXE
16531 INFO: Embedding manifest in EXE
16532 INFO: Updating manifest in C:\mrathena\develop\workspace\pycharm\python.apex.helper\build\apex\apex.exe.notanexecutable
16534 INFO: Updating resource type 24 name 1 language 0
16570 INFO: Appending PKG archive to EXE
16573 INFO: Fixing EXE headers
16732 INFO: Building EXE from EXE-00.toc completed successfully.
16735 INFO: checking COLLECT
16735 INFO: Building COLLECT because COLLECT-00.toc is non existent
16736 INFO: Building COLLECT COLLECT-00.toc
17544 INFO: Building COLLECT COLLECT-00.toc completed successfully.
(apex) C:\mrathena\develop\workspace\pycharm\python.apex.helper>
拓展 AI 目标检测, 移动鼠标, 彻底告别压枪
Python Apex Legends AI 自瞄 全过程记录