文章目录
本文为下面参考文章的学习与实践
环境准备
conda create -n apex python=3.9
操纵键鼠
由于绝地求生屏蔽了硬件驱动外的其他鼠标输入,因此我们无法直接通过py脚本来控制游戏内鼠标操作。为了实现游戏内的鼠标下移,我使用了罗技鼠标的驱动(ghub),而py通过调用ghub的链接库文件,将指令操作传递给ghub,最终实现使用硬件驱动的鼠标指令输入给游戏,从而绕过游戏的鼠标输入限制。值得一提的是,我们只是通过py代码调用链接库的接口将指令传递给罗技驱动的,跟实际使用的是何种鼠标没有关系,所以即便用户使用的是雷蛇、卓威、双飞燕等鼠标,对下面的代码并无任何影响。
驱动安装 链接库加载 代码准备和游戏外测试
罗技驱动使用 LGS_9.02.65_X64(请自行找资源安装,官网新版罗技驱动没找到对应的链接库文件),链接库文件在项目链接里面可以找到。下面是载入链接库的代码。
罗技驱动分LGS(老)和GHub(新), 必须装指定版本的LGS驱动(如已安装GHub可能需要卸载), 不然要么报未安装, 要么初始化成功但调用无效
try:
gm = CDLL(r'./mouse.device.lgs.dll')
gmok = gm.device_open() == 1
if not gmok:
print('未安装ghub或者lgs驱动!!!')
else:
print('初始化成功!')
except FileNotFoundError:
print('缺少文件')
装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 这个 dll 文件里面的方法都没有对应的文档, 只能猜测参数了
toolkit.py
import time
from ctypes import CDLL
import win32api # conda install pywin32
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = win32api.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
游戏内测试
在游戏里面试过后, 管用, 但是不准, 猜测可能和游戏内鼠标灵敏度/FOV等有关系
from toolkit import Mouse
import pynput # conda install pynput
def onClick(x, y, button, pressed):
if not pressed:
if pynput.mouse.Button.x2 == button:
Mouse.move(100, 100)
mouseListener = pynput.mouse.Listener(on_click=onClick)
mouseListener.start()
mouseListener.join()
键鼠监听
前面说到,要实现压枪就要对各种配件、状态做出识别。那么在写识别的函数之前,我们先要解决的是何时识别的问题。如果识别使用多线程\多进程的一直持续检测,无疑是一种巨大的开销,因此就需要对键盘、鼠标的状态进行监听。只有按下特定按键时,才触发特定相应的识别请求。
这里我使用的钩子是Pynput,其他可使用的库还有Pyhook3
def onClick(x, y, button, pressed):
print(f'button {button} {"pressed" if pressed else "released"} at ({x},{y})')
if pynput.mouse.Button.left == button:
return False # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.mouse.Listener(on_click=onClick)
listener.start()
def onRelease(key):
print(f'{key} released')
if key == pynput.keyboard.Key.end:
return False # 正常不要返回False, 这样会结束监听并停止监听线程, 在关闭程序前返回False就好了
listener = pynput.keyboard.Listener(on_release=onRelease)
listener.start()
注意调试回调方法的时候, 不要打断点, 不要打断点, 不要打断点, 这样会卡死IO, 导致鼠标键盘失效
Listener中绑定on_press和on_release的函数( on_key_press、on_key_release),它们返回False的时候是结束监听,下文鼠标监听的函数同理,所以不要随便返回False
键盘的特殊按键采用keyboard.Key.tab这种写法,普通按键用keyboard.KeyCode.from_char(‘c’)这种写法
有些键不知道该怎么写, 可以 print(key)
查看写法
这里有一点非常坑,on_press和on_release的参数只能有一个key,这个key就是对应键盘按下的哪颗按键。但这是不足以满足我们的需求的,因为我们应该在钩子函数内部,在按下指定按键时对信号量做出修改,但因为参数的限制,我们无法把信号量传进函数内部,这里我也是想了很久,最后才想到用嵌套函数的写法解决这个问题。
另外,钩子函数本身是阻塞的。也就是说钩子函数在执行的过程中,用户正常的键盘/鼠标操作是无法输入的。所以在钩子函数里面必须写成有限的操作(即O(1)时间复杂度的代码),也就是说像背包内配件及枪械识别,还有下文会讲到的鼠标压枪这类时间开销比较大或者持续时间长的操作,都不适合写在钩子函数里面。这也解释了为什么在检测到Tab(打开背包)、鼠标左键按下时,为什么只是改变信号量,然后把这些任务丢给别的进程去做的原因。
武器识别
如何简单且高效地判断是否在游戏内
先是判断游戏窗体是否在最前端, 然后判断游戏内是否正在持枪界面
找几个特征点取色判断, 如血条左上角和生存物品框左下角
一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定, 不会受不同背景色的影响
我原本以为屏幕点取色应该不会超过1ms的耗时, 结果万万没想到, 取个色居然要1-10ms, 效率奇低, 暂无其他优雅方法
如何简单且高效地判断背包状态 无武器/1号武器/2号武器
黄圈内的武器面板, 可以分为两个部分, 上边是边框, 下边是名字, 上边的边框又可以分为上半部分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是兴趣, 很多基础不到位, 相关专业知识更是空白, 参考内容也参差不齐, 导致对检测和训练的参数都很模糊. 二是数据集采集, 网上找了些, 自己做了些, 但是任然只有一点点, 不着急, 慢慢找吧. 据说要想效果好, 得几千张图片集 …
组织数据
武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息
配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息将配置分类
信号数据, 程序运行时, 进程线程间通讯
第一阶段实现 能自动识别出所有武器
目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不会超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了
我的配置: AMD R7 2700x, Nvidia RTX 2080, 34401440 分辨率
点位不好确认的话, 可以在该截图上找, 该截图就是 无边框 34401440 分辨率下的训练场截图
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack' # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet' # 子弹
differ = 'differ'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器名称上面边框分界线的上半部分, y+1 就是1号武器名称上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
point: (3148, 1349),
'0xf8f8f8': 1, # 全自动
'0xfefefe': 2 # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2983, 1384), # 2: 敖犬霰弹枪
(3003, 1383), # 3: 波塞克
(3014, 1383), # 4: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪',
},
'2': {
name: '转换者冲锋枪',
},
'3': {
name: 'R-301 卡宾枪',
},
'4': {
name: 'R-99 冲锋枪',
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
},
'2': {
name: '猎兽冲锋枪',
},
'3': {
name: '平行步枪',
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR能量机枪',
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
},
'4': {
name: '专注轻机枪',
},
'5': {
name: '哈沃克步枪',
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '敖犬霰弹枪',
},
'3': {
name: '波塞克',
},
'4': {
name: '暴走',
},
}
}
toolkit.py
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
hdc = user32.GetDC(None)
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx <= ady:
# 水平方向移动的距离短
for i in range(1, adx):
ix = i if mx > 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
# hdc = user32.GetDC(None)
return gdi32.GetPixel(hdc, x, y)
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = user32.GetSystemMetrics(0)
h = user32.GetSystemMetrics(1)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = user32.GetSystemMetrics(78)
h = user32.GetSystemMetrics(79)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
# hdc = user32.GetDC(None)
w = gdi32.GetDeviceCaps(hdc, 118)
h = gdi32.GetDeviceCaps(hdc, 117)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def game():
"""
是否在游戏内
太耗时了, 所以不能调的多了
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bullet = data.get(hex(color))
return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)
@staticmethod
def weapon(index, bullet):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param index: 武器位, 1:1号位, 2:2号位
:param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.name)
color = data.get(cfg.color)
if index == 1:
lst = data.get(str(index)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif index == 2:
differ = data.get(str(index)).get(cfg.differ)
lst = data.get(str(1)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.mode)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
return data.get(hex(color))
@staticmethod
def detect():
"""
决策是否需要压枪, 向信号量写数据
"""
if Game.game() is False:
print('not in game')
return
index, bullet = Game.index()
if (index is None) | (bullet is None):
print('no weapon')
return
if Game.mode() is None:
print('not in full auto or semi auto mode')
return
arms = Game.weapon(index, bullet)
if arms is None:
print('detect weapon failure')
return
# 检测通过, 需要压枪
print(weapon.get(str(bullet)).get(str(arms)).get(cfg.name))
return weapon.get(str(bullet)).get(str(arms)).get(cfg.name)
apex.py
import time
import pynput # conda install pynput
import toolkit
ExitFlag = False
def down(x, y, button, pressed):
global ExitFlag
if ExitFlag:
print(ExitFlag)
return False # 结束监听线程
if pressed: # 按下
if pynput.mouse.Button.right == button:
toolkit.Game.detect()
mouseListener = pynput.mouse.Listener(on_click=down)
mouseListener.start()
def release(key):
if key == pynput.keyboard.Key.end:
print('end')
global ExitFlag
ExitFlag = True
return False
if key == pynput.keyboard.KeyCode.from_char('1'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('2'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('3'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('e'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('v'):
toolkit.Game.detect()
keyboardListener = pynput.keyboard.Listener(on_release=release)
keyboardListener.start()
keyboardListener.join()
第二阶段实现 能自动采用对应抖枪参数执行压枪
- 游戏内鼠标灵敏度越高越容易抖枪且效果更好, 但是开到5的话, 会感到有点晕
- 游戏内鼠标灵敏度越高, 代码里抖动的像素就需要设置的更小, 比如5的灵敏度, 抖动2像素就可以了
- 抖枪能减小后坐力, 但不能完全消除, 所以还需配合对应方向的移动
在2.5灵敏度下, 301使用下面这个参数, 二三十米还行, 五十米, 三倍效果还将就一倍就很差了. 后坐力越大的武器, 前几枪容易跳太高, 下压力度可以大点
还有就是延迟要低一点, 我这边裸连延迟300+, 经常子弹打出去, 过半秒才减血, 这样很难测的准
能量武器, 专注和哈沃克, 预热和涡轮有很大影响, 这里暂时没管, 在第三阶段实现
total = 0 # 总计时 ms
delay = 1 # 延迟 ms
pixel = 4 # 抖动像素
while True:
if not data[fire]:
break
# 下压
if total < 30:
toolkit.Mouse.move(0, 5)
time.sleep(delay / 1000)
total += delay
else:
toolkit.Mouse.move(0, 1)
time.sleep(delay / 1000)
total += delay
# 抖枪
toolkit.Mouse.move(pixel, 0)
time.sleep(delay / 1000)
total += delay
toolkit.Mouse.move(-pixel, 0)
time.sleep(delay / 1000)
total += delay
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack' # 背包
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
bullet = 'bullet' # 子弹
differ = 'differ'
suppress = 'suppress'
strength = 'strength'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
color: 0x00FFFFFF,
'1': (3151, 1347), # 全自动
'2': (3171, 1351), # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2983, 1384), # 2: 敖犬霰弹枪
(3003, 1383), # 3: 波塞克
(3014, 1383), # 4: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪', # 全程往右飘
shake: {
speed: 80,
count: 10,
strength: 5,
}
},
'2': {
name: '转换者冲锋枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'3': {
name: 'R-301 卡宾枪',
shake: {
speed: 74, # 74ms打一发子弹
count: 6, # 压制前6发
strength: 5, # 压制的力度(下移的像素)
},
suppress: {
speed: 74,
}
},
'4': {
name: 'R-99 冲锋枪',
shake: {
speed: 55.5,
count: 13,
strength: 8,
}
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
shake: {
speed: 111,
count: 8,
strength: 5,
}
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
}
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
shake: {
speed: 50,
count: 3,
strength: 6,
}
},
'2': {
name: '猎兽冲锋枪',
shake: {
speed: 50,
count: 5,
strength: 6,
}
},
'3': {
name: '平行步枪',
shake: {
speed: 100,
count: 5,
strength: 5,
}
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
}
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR能量机枪',
shake: {
speed: 100,
count: 10,
strength: 5,
}
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
shake: {
speed: 83.3,
count: 10,
strength: 7,
}
},
'4': {
name: '专注轻机枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'5': {
name: '哈沃克步枪',
shake: {
speed: 100,
count: 8,
strength: 6,
}
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '敖犬霰弹枪',
},
'3': {
name: '波塞克',
},
'4': {
name: '暴走',
shake: {
speed: 200,
count: 8,
strength: 2,
}
},
}
}
toolkit.py
import time
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx <= ady:
# 水平方向移动的距离短
for i in range(1, adx):
ix = i if mx > 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
hdc = user32.GetDC(None)
return gdi32.GetPixel(hdc, x, y)
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = user32.GetSystemMetrics(0)
h = user32.GetSystemMetrics(1)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = user32.GetSystemMetrics(78)
h = user32.GetSystemMetrics(79)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
hdc = user32.GetDC(None)
w = gdi32.GetDeviceCaps(hdc, 118)
h = gdi32.GetDeviceCaps(hdc, 117)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def game():
"""
是否在游戏内(顶层窗口是游戏,且正在进行一局游戏,且游戏界面上有血条)
"""
# 先判断是否是游戏窗口
hwnd = user32.GetForegroundWindow()
length = user32.GetWindowTextLengthW(hwnd)
buffer = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buffer, length + 1)
if 'Apex Legends' != buffer.value:
return False
# 是在游戏中, 再判断下是否有血条和生存物品包
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bullet = data.get(hex(color))
return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)
@staticmethod
def weapon(index, bullet):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param index: 武器位, 1:1号位, 2:2号位
:param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.name)
color = data.get(cfg.color)
if index == 1:
lst = data.get(str(index)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif index == 2:
differ = data.get(str(index)).get(cfg.differ)
lst = data.get(str(1)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.mode)
color = data.get(cfg.color)
x, y = data.get('1')
if color == Monitor.pixel(x, y):
return 1
x, y = data.get('2')
if color == Monitor.pixel(x, y):
return 2
return None
@staticmethod
def detect(data):
"""
决策是否需要压枪, 向信号量写数据
"""
if data[cfg.switch] is False:
print('开关已关闭')
return
t1 = time.perf_counter_ns()
if Game.game() is False:
print('不在游戏中')
data[cfg.shake] = None
return
index, bullet = Game.index()
if (index is None) | (bullet is None):
print('没有武器')
data[cfg.shake] = None
return
if Game.mode() is None:
print('不是自动/半自动武器')
data[cfg.shake] = None
return
arms = Game.weapon(index, bullet)
if arms is None:
print('识别武器失败')
data[cfg.shake] = None
return
# 检测通过, 需要压枪
gun = weapon.get(str(bullet)).get(str(arms))
data[cfg.shake] = gun.get(cfg.shake) # 记录当前武器抖动参数
t2 = time.perf_counter_ns()
print(f'耗时:{t2-t1}ns, 约{(t2-t1)//1000000}ms, {gun.get(cfg.name)}')
apex.py
import multiprocessing
import time
from multiprocessing import Process
import pynput # conda install pynput
import toolkit
end = 'end'
fire = 'fire'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
strength = 'strength'
init = {
end: False, # 退出标记, End 键按下后改为 True, 其他进程线程在感知到变更后结束自身
switch: True, # 开关
fire: False, # 开火状态
shake: None, # 抖枪参数
}
def listener(data):
def down(x, y, button, pressed):
if data[end]:
return False # 结束监听线程
if button == pynput.mouse.Button.right:
if pressed:
toolkit.Game.detect(data)
elif button == pynput.mouse.Button.left:
data[fire] = pressed
mouse = pynput.mouse.Listener(on_click=down)
mouse.start()
def release(key):
if key == pynput.keyboard.Key.end:
# 结束程序
data[end] = True
return False
elif key == pynput.keyboard.Key.home:
# 压枪开关
data[switch] = not data[switch]
elif key == pynput.keyboard.Key.esc:
toolkit.Game.detect(data)
elif key == pynput.keyboard.Key.tab:
toolkit.Game.detect(data)
elif key == pynput.keyboard.KeyCode.from_char('1'):
toolkit.Game.detect(data)
elif key == pynput.keyboard.KeyCode.from_char('2'):
toolkit.Game.detect(data)
elif key == pynput.keyboard.KeyCode.from_char('3'):
toolkit.Game.detect(data)
elif key == pynput.keyboard.KeyCode.from_char('e'):
toolkit.Game.detect(data)
elif key == pynput.keyboard.KeyCode.from_char('v'):
toolkit.Game.detect(data)
keyboard = pynput.keyboard.Listener(on_release=release)
keyboard.start()
keyboard.join() # 卡住监听进程, 当键盘线程结束后, 监听进程才能结束
def suppress(data):
while True:
if data[end]:
break
if data[switch] is False:
continue
if data[fire] & (data[shake] is not None):
# 301 大约75ms一发子弹
total = 0 # 总计时 ms
delay = 1 # 延迟 ms
pixel = 4 # 抖动像素
while True:
if not data[fire]:
break
# 下压
t = time.perf_counter_ns()
if total < data[shake][speed] * data[shake][count]:
toolkit.Mouse.move(0, data[shake][strength])
time.sleep(delay / 1000)
total += delay
else:
toolkit.Mouse.move(0, 1)
time.sleep(delay / 1000)
total += delay
# 抖枪
toolkit.Mouse.move(pixel, 0)
time.sleep(delay / 1000)
total += delay
toolkit.Mouse.move(-pixel, 0)
time.sleep(delay / 1000)
total += delay
total += (time.perf_counter_ns() - t) // 1000 // 1000
if __name__ == '__main__':
multiprocessing.freeze_support() # windows 平台使用 multiprocessing 必须在 main 中第一行写这个
manager = multiprocessing.Manager()
data = manager.dict() # 创建进程安全的共享变量
data.update(init) # 将初始数据导入到共享变量
# 将键鼠监听和压枪放到单独进程中跑
p1 = Process(target=listener, args=(data,)) # 监听进程
p2 = Process(target=suppress, args=(data,)) # 压枪进程
p1.start()
p2.start()
p1.join() # 卡住主进程, 当进程 listener 结束后, 主进程才会结束
第三阶段实现 放弃抖枪术 转常规后座抵消法
常用的武器大概都简单调了下, 包括有无涡轮, 有无双发扳机. 有压枪参数的使用压枪参数, 其他的使用抖枪参数, 压枪与抖枪并存
我的游戏内鼠标设置是这样的, 要确保每个瞄镜的ADS都是一样的, 鼠标DPI是3200
最终的效果是, 20米前一半子弹很稳, 30米将就, 50米不太行, 有几率一梭子打倒, 再往后就没意义了. 差不多够用了, 就没再认真调
如何调压枪参数
我觉得调参数最重要的一点, 就是先算出正确的子弹射速(平均每发子弹耗时), 如果用了错误的数据, 那很可能调了半天白费功夫
测试方法我总结了下, 首先, 每发子弹耗时通常都是50到150毫秒, 先假设是100, 看有多少发子弹, 就复制多少条压枪数据, 举例
R-301 这把枪, 加上金扩容, 28发子弹, 那就先准备下面的初始数据, 三个参数分别是, 鼠标水平移动的值/垂直移动的值/移动后休眠时间, 当然也可以有其他的参数
先把对应最后一发子弹的鼠标移动值设置为10000, 看是否打完子弹时, 鼠标正好产生大幅位移, 然后调后面的100, 直到恰好匹配, 然后就可以开始调鼠标参数了
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100],
[0, 0, 100], #
[0, 0, 100],
[0, 0, 100],
[10000, 0, 100],
调鼠标参数时, 要从上往下逐个调, 因为上面的一个变动, 对下面的影响非常大, 很可能导致下面的白调了
比如调纵向压制的时候, 1倍镜30米瞄这这道杠打, 争取基本全都在杠上, 纵向就ok了, 横向同理
也可以借助录像工具, 录制屏幕中心部分区域, 然后以0.1倍速播放, 仔细查看压制力度是否合适
最终的效果就是, 不太稳定, 123倍镜表现不太一致, 3倍镜偏差最大. 难不成各个镜子做一套参数?
游戏中实测
整体来说, 表现还行吧. 不过距离一条线还差得远
存在的问题
- 采用取色判断法, 单点取色耗时1-10ms, 性能不足 (已有优化思路, 一种是, 通过GetCurrentObject和GetObject获取和hdc相关的位图对象数据区起始地址, 拿到BitMap, 直接取对应坐标的颜色. 另一种是, 通过 d3dshot 模块来高效截图并取点. 受限于个人水平, 暂无法用Python实现)
- 检测武器名称使用的是O(n)时间复杂度的遍历方式, 在取色判断法效率低的情况下, 性能不够优秀和稳定, 期望做到O(1)
- 暂无法判断是否持有武器(有武器但我用拳头, 可能引起错误地触发压枪)
- 暂无法实现按着左键时模拟左键点击效果, 所以暂无法实现单发枪变连发枪的功能
详细代码
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'
color = 'color'
point = 'point'
index = 'index'
shake = 'shake'
speed = 'speed'
count = 'count'
armed = 'armed'
empty = 'empty'
switch = 'switch'
bullet = 'bullet' # 子弹
differ = 'differ'
turbo = 'turbo'
trigger = 'trigger'
restrain = 'restrain'
strength = 'strength'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
color: 0x00FFFFFF,
'1': (3151, 1347), # 全自动
'2': (3171, 1351), # 半自动
},
armed: { # 是否持有武器(比如有武器但用拳头就是未持有武器)
},
empty: { # 是否空弹夹(武器里子弹数为0)
color: 0x00FFFFFF,
'1': (3204, 1306), # 十位数, 该点白色即非0, 非0则一定不空
'2': (3229, 1294), # 个位数, 该点白色即为0, 十位为0且个位为0为空
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR 能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2959, 1384), # 2: 手感卓越的刀刃
(2983, 1384), # 3: 敖犬霰弹枪
(3003, 1383), # 4: 波塞克
(3014, 1383), # 5: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
},
turbo: { # 涡轮
color: 0x00FFFFFF,
'3': {
differ: 2, # 有涡轮和没涡轮的索引偏移
'4': (3072, 1358), # 专注轻机枪 涡轮检测位置
'5': (3034, 1358), # 哈沃克步枪 涡轮检测位置
}
},
trigger: { # 双发扳机
color: 0x00FFFFFF,
'1': {
differ: 2,
'7': (3072, 1358), # G7 侦查枪 双发扳机检测位置
},
'5': {
differ: 1,
'3': (3034, 1358), # EVA-8 双发扳机检测位置
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪', # 全程往右飘
shake: {
speed: 80,
count: 10,
strength: 5,
},
restrain: [
[1, -2, 10, 80], #
[1, -2, 10, 80],
[1, -2, 10, 80],
[1, -4, 10, 80],
[1, -6, 10, 80],
[1, -7, 8, 80], #
[1, -7, 8, 80],
[1, -7, 8, 80],
[1, -7, 8, 80],
[1, -7, 8, 80],
[1, -1, 5, 80], #
[1, -1, 5, 80],
[1, -1, 5, 80],
[1, -1, 5, 80],
[1, -1, 5, 80],
[1, -1, 5, 80], #
[1, -1, 3, 80],
[1, -1, 3, 80],
[1, -1, 3, 80],
[1, -1, 3, 80],
[1, -1, 3, 80], #
[1, -2, 3, 80],
[1, -2, 3, 80],
[1, -2, 3, 80],
[1, -2, 3, 80],
[1, -5, 3, 80], #
[1, -5, 3, 80],
[1, -10, 3, 80],
[1, -10, 3, 80],
[1, -10, 3, 80],
]
},
'2': {
name: '转换者冲锋枪',
shake: {
speed: 100,
count: 10,
strength: 7,
},
restrain: [
[1, 0, 15, 94],
[1, 0, 15, 94],
[1, 0, 15, 94],
[1, 0, 15, 94],
[1, 0, 15, 94], #
[1, 0, 15, 94],
[1, 0, 15, 94],
[1, 0, 10, 94],
[1, 0, 10, 94],
[1, 0, 10, 94], #
[1, -5, 5, 94],
[1, -5, 5, 94],
[1, -5, 5, 94],
[1, 0, 5, 94],
[1, 0, 5, 94], #
[1, 0, 5, 94],
[1, 5, 5, 94],
[1, 5, 5, 94],
[1, 5, 5, 94],
[1, 0, 5, 94], #
[1, 0, 5, 94],
[1, 0, 5, 94],
[1, 0, 5, 94],
[1, 0, 5, 94],
[1, 0, 5, 94], #
[1, 0, 5, 94],
[1, 0, 0, 94],
]
},
'3': {
name: 'R-301 卡宾枪',
shake: {
speed: 64, # 74ms打一发子弹
count: 6, # 压制前6发
strength: 5, # 压制的力度(下移的像素)
},
restrain: [
[1, -5, 10, 70],
[1, 0, 10, 70],
[1, -5, 10, 70],
[1, -2, 10, 70],
[1, 0, 10, 70], #
[1, 0, 5, 70],
[1, 0, 0, 70],
[1, -5, 0, 70],
[1, -5, 5, 70],
[1, 0, 0, 70], #
[1, 0, 0, 70],
[1, 5, 10, 70],
[1, 5, 5, 70],
[1, 5, 0, 70],
[1, 5, 0, 70], #
[1, 0, 0, 70],
[1, 5, 0, 70],
[1, 5, 10, 70],
[1, 0, 10, 70],
[1, -5, 0, 70], #
[1, -5, 0, 70],
[1, -5, 0, 70],
[1, -5, 0, 70],
[1, -5, 0, 70],
[1, 0, 0, 70], #
[1, 0, 0, 70],
[1, 0, 0, 70],
[1, 0, 0, 64],
]
},
'4': {
name: 'R-99 冲锋枪',
shake: {
speed: 55.5,
count: 13,
strength: 8,
},
restrain: [
[1, 0, 10, 48],
[1, 0, 10, 48],
[1, 0, 10, 48],
[1, -5, 10, 48],
[1, -5, 10, 48], #
[1, -5, 10, 48],
[1, -5, 10, 48],
[1, 0, 10, 48],
[1, 0, 10, 48],
[1, 0, 10, 48], #
[1, 5, 10, 48],
[1, 5, 10, 48],
[1, 5, 10, 48],
[1, 0, 10, 48],
[1, 0, 0, 48], #
[1, -5, 0, 48],
[1, -10, 0, 48],
[1, 0, 0, 48],
[1, 0, 0, 48],
[1, 5, 5, 48], #
[1, 10, 5, 48],
[1, 10, 5, 48],
[1, 5, 0, 48],
[1, 0, 0, 48],
[1, -5, 0, 48], #
[1, -5, 0, 48],
[1, -5, 0, 48],
]
},
'5': {
name: 'P2020 手枪',
restrain: [
[2, 1, 100],
]
},
'6': {
name: '喷火轻机枪',
shake: {
speed: 110,
count: 8,
strength: 5,
},
restrain: [
[1, 0, 20, 100],
[1, 5, 15, 100],
[1, 5, 15, 100],
[1, 5, 15, 100],
[1, 5, 15, 100], #
[1, 5, 15, 100],
[1, -5, 10, 100],
[1, -5, 0, 100],
[1, -5, 0, 100],
[1, -5, 0, 100], #
[1, 0, 0, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 5, 5, 100],
[1, 10, 5, 100], #
[1, 10, 5, 100],
[1, 5, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100], # 20
[1, 0, 0, 100],
[1, 0, 0, 100],
[1, 0, 0, 100],
[1, 0, 0, 100],
[1, -5, 5, 100], #
[1, -5, 5, 100],
[1, -5, 5, 100],
[1, -5, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100], #
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100], #
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100], #
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100], #
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 5, 100],
[1, 0, 0, 100], #
]
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
shake: {
speed: 64.5,
count: 10,
strength: 7,
},
restrain: [
[1, 0, 10, 58], #
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58], #
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, -5, 10, 58],
[1, -5, 10, 58],
[1, -5, 5, 58], #
[1, -5, 10, 58],
[1, -5, 0, 58],
[1, 0, 0, 58],
[1, 5, 0, 58],
[1, 5, 3, 58], #
[1, 5, 3, 58],
[1, -5, 3, 58],
[1, -5, 3, 58],
[1, -5, 3, 58],
[1, 0, 0, 58], #
[1, 0, 0, 58],
[1, 0, 0, 58],
[1, 0, 3, 58],
[1, 0, 3, 58],
[1, 0, 3, 58], #
[1, 0, 3, 58],
]
},
'9': {
name: 'G7 侦查枪 (双发扳机)',
restrain: [
[1, 0, 5, 20]
]
},
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
shake: {
speed: 50,
count: 3,
strength: 6,
}
},
'2': {
name: '猎兽冲锋枪',
shake: {
speed: 50,
count: 5,
strength: 6,
}
},
'3': {
name: '平行步枪',
shake: {
speed: 100,
count: 5,
strength: 5,
},
restrain: [
[1, 0, 10, 100], #
[1, 5, 10, 100],
[1, 5, 10, 100],
[1, 5, 10, 100],
[1, 5, 10, 100],
[1, -5, 10, 100], #
[1, -5, 0, 100],
[1, -5, 0, 100],
[1, -5, 0, 100],
[1, 0, 5, 100],
[1, 5, 5, 100], #
[1, 5, 5, 100],
[1, 5, 0, 100],
[1, 5, 0, 100],
[1, 0, 0, 100],
[1, 5, 5, 100], #
[1, 5, 5, 100],
[1, 5, 5, 100],
[1, 0, 0, 100],
[1, 0, 0, 100],
[1, -5, 5, 100], #
[1, -5, 5, 100],
[1, -5, 5, 100],
[1, -0, 5, 100],
[1, 5, 5, 100],
[1, 5, 5, 100], #
[1, 5, 5, 100],
[1, -5, -5, 100],
[1, -5, 5, 100],
[1, -5, 5, 100],
]
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
shake: {
speed: 58,
count: 10,
strength: 7,
},
restrain: [
[1, 0, 10, 58], #
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, 3, 10, 58], #
[1, 3, 10, 58],
[1, 3, 10, 58],
[1, -5, 10, 58],
[1, -5, 10, 58],
[1, -5, 5, 58], #
[1, -5, 10, 58],
[1, -5, 0, 58],
[1, 0, 0, 58],
[1, 5, 0, 58],
[1, 5, 3, 58], #
[1, 5, 3, 58],
[1, -5, 3, 58],
[1, -5, 3, 58],
[1, -5, 3, 58],
[1, 0, 0, 58], #
[1, 0, 0, 58],
[1, 0, 0, 58],
[1, 0, 3, 58],
[1, 0, 3, 58],
[1, 0, 3, 58], #
[1, 0, 3, 58],
]
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR 能量机枪',
shake: {
speed: 100,
count: 10,
strength: 5,
}
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
shake: {
speed: 83.3,
count: 10,
strength: 7,
},
restrain: [
[1, -5, 15, 80],
[1, 0, 15, 80],
[1, 0, 15, 80],
[1, 0, 15, 80],
[1, 0, 15, 80], #
[1, -5, 10, 80],
[1, -5, 10, 80],
[1, -5, 10, 80],
[1, 0, 10, 80],
[1, 5, 10, 80], #
[1, 5, 5, 80],
[1, 5, 5, 80],
[1, 5, 5, 80],
[1, 0, 5, 80],
[1, 0, 5, 80], #
[1, 0, 5, 80],
[1, 0, 0, 80],
[1, 0, 0, 80],
[1, 0, 0, 80],
[1, 0, 0, 80], #
[1, 0, 0, 80],
[1, 5, 0, 80],
[1, 5, 0, 80],
[1, 5, 0, 80],
[1, 0, 0, 80], #
[1, 0, 0, 80],
]
},
'4': {
name: '专注轻机枪',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'5': {
name: '哈沃克步枪',
shake: {
speed: 100,
count: 8,
strength: 6,
},
restrain: [
[1, 0, 0, 400], # 延迟
[1, -5, 10, 88], # 1
[1, -5, 15, 88],
[1, 0, 15, 88],
[1, 0, 15, 88],
[1, 0, 15, 88],
[1, 5, 10, 88], #
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, -5, 5, 88],
[1, -5, 0, 88], # 1
[1, -5, 0, 88],
[1, -10, 0, 88],
[1, -10, 0, 88],
[1, -5, 0, 88],
[1, 0, 5, 88], #
[1, 10, 5, 88],
[1, 10, 5, 88],
[1, 0, 0, 88],
[1, 0, 0, 88],
[1, 5, 10, 88], # 1
[1, 5, 10, 88],
[1, 0, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88], #
[1, 5, 5, 88],
[1, 5, 5, 88],
[1, 0, 5, 88],
[1, 0, 0, 88],
[1, 0, 0, 88], # 1
[1, 0, 0, 88],
[1, 0, 5, 88],
[1, 0, 5, 88],
[1, 0, 5, 88],
[1, 0, 5, 88], #
]
},
'6': {
name: '专注轻机枪 (涡轮)',
shake: {
speed: 100,
count: 10,
strength: 7,
}
},
'7': {
name: '哈沃克步枪 (涡轮)',
shake: {
speed: 100,
count: 8,
strength: 6,
},
restrain: [
[1, -5, 10, 88], # 1
[1, -5, 15, 88],
[1, 0, 15, 88],
[1, 0, 15, 88],
[1, 0, 15, 88],
[1, 5, 10, 88], #
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, -5, 5, 88],
[1, -5, 0, 88], # 1
[1, -5, 0, 88],
[1, -10, 0, 88],
[1, -10, 0, 88],
[1, -5, 0, 88],
[1, 0, 5, 88], #
[1, 10, 5, 88],
[1, 10, 5, 88],
[1, 0, 0, 88],
[1, 0, 0, 88],
[1, 5, 10, 88], # 1
[1, 5, 10, 88],
[1, 0, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88],
[1, 5, 10, 88], #
[1, 5, 5, 88],
[1, 5, 5, 88],
[1, 0, 5, 88],
[1, 0, 0, 88],
[1, 0, 0, 88], # 1
[1, 0, 0, 88],
[1, 0, 5, 88],
[1, 0, 5, 88],
[1, 0, 5, 88],
[1, 0, 5, 88], #
]
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
'4': {
name: 'EVA-8 (双发扳机)',
}
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '手感卓越的刀刃',
},
'3': {
name: '敖犬霰弹枪',
},
'4': {
name: '波塞克',
},
'5': {
name: '暴走',
shake: {
speed: 200,
count: 8,
strength: 2,
}
},
}
}
toolkit.py
import time
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
if (x == 0) & (y == 0):
return
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx <= ady:
# 水平方向移动的距离短
for i in range(1, adx):
ix = i if mx > 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
hdc = user32.GetDC(None)
color = gdi32.GetPixel(hdc, x, y)
user32.ReleaseDC(None, hdc) # 一定要释放DC, 不然随着该函数调用次数增加会越来越卡, 表现就是不调用该函数, 系统会每两秒卡一下, 调用次数越多, 卡的程度越厉害
return color
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)
user32.ReleaseDC(None, hdc)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def key():
w, h = Monitor.Resolution.display()
return f'{w}:{h}'
@staticmethod
def game():
"""
是否游戏窗体在最前
"""
# 先判断是否是游戏窗口
hwnd = user32.GetForegroundWindow()
length = user32.GetWindowTextLengthW(hwnd)
buffer = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buffer, length + 1)
if 'Apex Legends' != buffer.value:
return False
return True
@staticmethod
def play():
"""
是否正在玩
"""
# 是在游戏中, 再判断下是否有血条和生存物品包
data = detect.get(Game.key()).get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
data = detect.get(Game.key()).get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bi = data.get(hex(color))
return (1, bi) if color == Monitor.pixel(x, y + 1) else (2, bi)
@staticmethod
def weapon(pi, bi):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param pi: 武器位, 1:1号位, 2:2号位
:param bi: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
data = detect.get(Game.key()).get(cfg.name)
color = data.get(cfg.color)
if pi == 1:
lst = data.get(str(pi)).get(str(bi))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif pi == 2:
differ = data.get(str(pi)).get(cfg.differ)
lst = data.get(str(1)).get(str(bi))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
data = detect.get(Game.key()).get(cfg.mode)
color = data.get(cfg.color)
x, y = data.get('1')
if color == Monitor.pixel(x, y):
return 1
x, y = data.get('2')
if color == Monitor.pixel(x, y):
return 2
return None
@staticmethod
def armed():
"""
是否持有武器
"""
return True
@staticmethod
def empty():
"""
是否空弹夹
"""
data = detect.get(Game.key()).get(cfg.empty)
color = data.get(cfg.color)
x, y = data.get('1')
if color == Monitor.pixel(x, y):
return False
x, y = data.get('2')
return color == Monitor.pixel(x, y)
@staticmethod
def turbo(bi, wi):
"""
判断是否有涡轮, 只有配置了检测涡轮的武器才会做取色判断
:return: (False, None), (True, differ), 有涡轮的话, 额外返回涡轮索引偏移
"""
data = detect.get(Game.key()).get(cfg.turbo)
color = data.get(cfg.color)
data = data.get(str(bi))
if data is None:
return False, None
differ = data.get(cfg.differ)
data = data.get(str(wi))
if data is None:
return False, None
x, y = data
result = color == Monitor.pixel(x, y)
return (True, differ) if result else (False, None)
@staticmethod
def trigger(bi, wi):
"""
判断是否有双发扳机, 只有配置了检测双发扳机的武器才会做取色判断
:return: (False, None), (True, differ), 有双发扳机的话, 额外返回双发扳机索引偏移
"""
data = detect.get(Game.key()).get(cfg.trigger)
color = data.get(cfg.color)
data = data.get(str(bi))
if data is None:
return False, None
differ = data.get(cfg.differ)
data = data.get(str(wi))
if data is None:
return False, None
x, y = data
result = color == Monitor.pixel(x, y)
return (True, differ) if result else (False, None)
@staticmethod
def detect(data):
"""
决策是否需要压枪, 向信号量写数据
"""
t1 = time.perf_counter_ns()
if data.get(cfg.switch) is False:
t2 = time.perf_counter_ns()
print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 开关已关闭')
return
if Game.game() is False:
data[cfg.shake] = None
data[cfg.restrain] = None
t2 = time.perf_counter_ns()
print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 不在游戏中')
return
if Game.play() is False:
data[cfg.shake] = None
data[cfg.restrain] = None
t2 = time.perf_counter_ns()
print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 不在游戏中')
return
pi, bi = Game.index()
if (pi is None) | (bi is None):
data[cfg.shake] = None
data[cfg.restrain] = None
t2 = time.perf_counter_ns()
print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 没有武器')
return
# if Game.mode() is None:
# data[cfg.shake] = None
# data[cfg.restrain] = None
# t2 = time.perf_counter_ns()
# print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 不是自动/半自动武器')
# return
wi = Game.weapon(pi, bi)
if wi is None:
data[cfg.shake] = None
data[cfg.restrain] = None
t2 = time.perf_counter_ns()
print(f'耗时: {t2 - t1}ns, 约{(t2 - t1) // 1000000}ms, 识别武器失败')
return
# 检测通过, 需要压枪
# 检测涡轮
result, differ = Game.turbo(bi, wi)
if result is False:
# 检测双发扳机
result, differ = Game.trigger(bi, wi)
# 拿对应参数
gun = weapon.get(str(bi)).get(str((wi + differ) if result else wi))
data[cfg.shake] = gun.get(cfg.shake) # 记录当前武器抖动参数
data[cfg.restrain] = gun.get(cfg.restrain) # 记录当前武器压制参数
t2 = time.perf_counter_ns()
print(f'耗时: {t2-t1}ns, 约{(t2-t1)//1000000}ms, {gun.get(cfg.name)}')
apex.py
import multiprocessing
import time
from multiprocessing import Process
import pynput # conda install pynput
from toolkit import Mouse, Game
end = 'end'
fire = 'fire'
shake = 'shake'
speed = 'speed'
count = 'count'
switch = 'switch'
restart = 'restart'
restrain = 'restrain'
strength = 'strength'
init = {
end: False, # 退出标记, End 键按下后改为 True, 其他进程线程在感知到变更后结束自身
switch: True, # 检测和压枪开关
fire: False, # 开火状态
shake: None, # 抖枪参数
restrain: None, # 压枪参数
}
def 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
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)
time.sleep((delay - (t2 - t1) // 1000 // 1000) / 1000)
elif operation == 2:
temp, code, delay = item
Mouse.click(code)
time.sleep((delay - (t2 - t1) // 1000 // 1000) / 1000)
elif data.get(shake) is not None:
total = 0 # 总计时 ms
delay = 1 # 延迟 ms
pixel = 4 # 抖动像素
while True:
if not data[fire]: # 停止开火
break
if not Game.game(): # 不在游戏中
break
if not Game.armed(): # 未持有武器
break
if Game.empty(): # 弹夹为空
break
t = time.perf_counter_ns()
if total < data[shake][speed] * data[shake][count]:
Mouse.move(0, data[shake][strength])
time.sleep(delay / 1000)
total += delay
else:
Mouse.move(0, 1)
time.sleep(delay / 1000)
total += delay
# 抖枪
Mouse.move(pixel, 0)
time.sleep(delay / 1000)
total += delay
Mouse.move(-pixel, 0)
time.sleep(delay / 1000)
total += delay
total += (time.perf_counter_ns() - t) // 1000 // 1000
if __name__ == '__main__':
multiprocessing.freeze_support() # windows 平台使用 multiprocessing 必须在 main 中第一行写这个
manager = multiprocessing.Manager()
data = manager.dict() # 创建进程安全的共享变量
data.update(init) # 将初始数据导入到共享变量
# 将键鼠监听和压枪放到单独进程中跑
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()
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
(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>
在工程下会多出来一个 dist/apex 目录, 里面就是打包好的程序和依赖了, 将整个文件夹打包成压缩包即可分享(暂未在其他电脑上验证)
第四阶段实现 AI 目标检测, 移动鼠标, 彻底告别压枪
转载自CSDN-专业IT技术社区
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/mrathena/article/details/126918389