Python Apex Legends 武器自动识别与压枪 全过程记录
文章目录
本文为下面参考文章的学习与实践
环境准备
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'./ghub_device.dll')
gmok = gm.device_open() == 1
if not gmok:
print('未安装ghub或者lgs驱动!!!')
else:
print('初始化成功!')
except FileNotFoundError:
print('缺少文件')
装了该驱动后, 无需重启电脑, 当下就生效了. 遗憾的是, 没有对应的文档, 只能猜测参数了
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’)这种写法
这里有一点非常坑,on_press和on_release的参数只能有一个key,这个key就是对应键盘按下的哪颗按键。但这是不足以满足我们的需求的,因为我们应该在钩子函数内部,在按下指定按键时对信号量做出修改,但因为参数的限制,我们无法把信号量传进函数内部,这里我也是想了很久,最后才想到用嵌套函数的写法解决这个问题。
另外,钩子函数本身是阻塞的。也就是说钩子函数在执行的过程中,用户正常的键盘/鼠标操作是无法输入的。所以在钩子函数里面必须写成有限的操作(即O(1)时间复杂度的代码),也就是说像背包内配件及枪械识别,还有下文会讲到的鼠标压枪这类时间开销比较大或者持续时间长的操作,都不适合写在钩子函数里面。这也解释了为什么在检测到Tab(打开背包)、鼠标左键按下时,为什么只是改变信号量,然后把这些任务丢给别的进程去做的原因。
武器识别
如何简单且高效判断是否在游戏内
找几个特征点取色判断, 血条左上角和生存物品框左下角
一般能用于取色的点, 它的颜色RGB都是相同的, 这种点的颜色非常稳定
我原本以为屏幕点取色应该不会超过1ms的耗时, 结果万万没想到, 取个色居然要1-10ms, 效率奇低, 暂无其他优雅方法
如何简单且高效判断背包状态 无武器/1号武器/2号武器
看武器边框上红色圈住的部分颜色, 灰色说明没有武器, 上下不同色说明使用2号武器, 上下同色说明使用1号武器
如何简单且高效判断武器子弹类别
可以和上面的放在一起, 同一个点直接判断出背包状态和武器子弹类别, 因为不同子弹类型的武器, 边框颜色不一样
如何简单且高效判断武器名称
在分类后的基础上, 通过 背包状态 确定要检查颜色的位置(1号位/2号位), 通过 武器子弹类别 缩小判断范围, 在每个武器的名字上找一个纯白色的点, 确保这个点只有这把武器是纯白色, 然后逐个对比
如何简单且高效判断武器模式 全自动/连发/单发
需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(后面有可能做自动单发, 到时候在考虑), 喷子和狙不需要压枪
所以需要找一个能区分三种模式的点(不同模式这个点的颜色不同但是稳定), 且这个点不能受和平和三重的特殊标记影响
收起武器, 部分武器可以通过[V]标判断, 放弃
何时触发识别
键盘 1/2/3/E/V 释放, 鼠标 右键 按下, 如果不影响开枪就右键了(测过了不影响), 影响的话就改成侧下键. 键位和键在游戏内的功能不冲突的
end 键释放, 结束程序
压枪思路
apex 的压枪有3个思路, 因为 apex 不同武器的弹道貌似是固定的, 没有随机值?, 其他游戏也是??
可以通过取巧的方式, 只做无配件状态下的反向抵消, 还省了找配件的麻烦
这种方法太难太麻烦了, 但是做的好的话, 基本一条线, 强的离谱
我先试试 抖枪大法
组织数据
武器数据, 通过子弹类型分组, 组里的每个成员指定序号, 名称, 压枪参数等信息
配置数据, 按分辨率分组, 再按是否在游戏中, 是否有武器, 武器位置, 武器子弹类型, 武器索引等信息分类
信号数据, 程序运行时, 进程线程间通讯
第一阶段实现 能自动识别出所有武器
目前测试下来, 一波识别大概六七十毫秒的样子, 最多也不会超过一百毫秒, 主要耗时在取色函数(1-10ms), 性能已经够用了
我的配置: AMD R7 2700x, Nvidia RTX 2080, 3440*1440 分辨率
cfg.py
mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack' # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet' # 子弹
differ = 'differ'
positive = 'positive' # 肯定的
negative = 'negative' # 否定的
# 检测数据
detect = {
"3440:1440": {
game: [ # 判断是否在游戏中
{
point: (236, 1344), # 点的坐标, 血条左上角
color: 0x00FFFFFF # 点的颜色, 255, 255, 255
},
{
point: (2692, 1372), # 生存物品右下角
color: 0x959595 # 149, 149, 149
}
],
pack: { # 背包状态, 有无武器, 选择的武器
point: (2900, 1372), # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
color: 0x808080, # 无武器时, 灰色, 128, 128, 128
'0x447bb4': 1, # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
'0x839b54': 2, # 重型弹药武器
'0x3da084': 3, # 能量弹药武器
'0xce5f6e': 4, # 狙击弹药武器
'0xf339b': 5, # 霰弹枪弹药武器
'0x5302ff': 6, # 空投武器
},
mode: { # 武器模式, 全自动/半自动/单发/其他
point: (3148, 1349),
'0xf8f8f8': 1, # 全自动
'0xfefefe': 2 # 半自动
},
name: { # 武器名称判断
color: 0x00FFFFFF,
'1': { # 1号武器
'1': [ # 轻型弹药武器
(2959, 1386), # 1: RE-45 自动手枪
(2970, 1385), # 2: 转换者冲锋枪
(2972, 1386), # 3: R-301 卡宾枪
(2976, 1386), # 4: R-99 冲锋枪
(2980, 1386), # 5: P2020 手枪
(2980, 1384), # 6: 喷火轻机枪
(2987, 1387), # 7: G7 侦查枪
(3015, 1386), # 8: CAR (轻型弹药)
],
'2': [ # 重型弹药武器
(2957, 1385), # 1: 赫姆洛克突击步枪
(2982, 1385), # 2: 猎兽冲锋枪
(2990, 1393), # 3: 平行步枪
(3004, 1386), # 4: 30-30
(3015, 1386), # 5: CAR (重型弹药)
],
'3': [ # 能量弹药武器
(2955, 1386), # 1: L-STAR能量机枪
(2970, 1384), # 2: 三重式狙击枪
(2981, 1385), # 3: 电能冲锋枪
(2986, 1384), # 4: 专注轻机枪
(2980, 1384), # 5: 哈沃克步枪
],
'4': [ # 狙击弹药武器
(2969, 1395), # 1: 哨兵狙击步枪
(2999, 1382), # 2: 充能步枪
(2992, 1385), # 3: 辅助手枪
(3016, 1383), # 4: 长弓
],
'5': [ # 霰弹枪弹药武器
(2957, 1384), # 1: 和平捍卫者霰弹枪
(2995, 1382), # 2: 莫桑比克
(3005, 1386), # 3: EVA-8
],
'6': [ # 空投武器
(2958, 1384), # 1: 克雷贝尔狙击枪
(2983, 1384), # 2: 敖犬霰弹枪
(3003, 1383), # 3: 波塞克
(3014, 1383), # 4: 暴走
]
},
'2': {
differ: 195 # 直接用1的坐标, 横坐标右移195就可以了
}
}
},
"2560:1440": {
},
"2560:1080": {
},
"1920:1080": {
}
}
# 武器数据
weapon = {
'1': { # 轻型弹药武器
'1': {
name: 'RE-45 自动手枪',
},
'2': {
name: '转换者冲锋枪',
},
'3': {
name: 'R-301 卡宾枪',
},
'4': {
name: 'R-99 冲锋枪',
},
'5': {
name: 'P2020 手枪',
},
'6': {
name: '喷火轻机枪',
},
'7': {
name: 'G7 侦查枪',
},
'8': {
name: 'CAR (轻型弹药)',
}
},
'2': { # 重型弹药武器
'1': {
name: '赫姆洛克突击步枪',
},
'2': {
name: '猎兽冲锋枪',
},
'3': {
name: '平行步枪',
},
'4': {
name: '30-30',
},
'5': {
name: 'CAR (重型弹药)',
}
},
'3': { # 能量弹药武器
'1': {
name: 'L-STAR能量机枪',
},
'2': {
name: '三重式狙击枪',
},
'3': {
name: '电能冲锋枪',
},
'4': {
name: '专注轻机枪',
},
'5': {
name: '哈沃克步枪',
},
},
'4': { # 狙击弹药武器
'1': {
name: '哨兵狙击步枪',
},
'2': {
name: '充能步枪',
},
'3': {
name: '辅助手枪',
},
'4': {
name: '长弓',
},
},
'5': { # 霰弹弹药武器
'1': {
name: '和平捍卫者霰弹枪',
},
'2': {
name: '莫桑比克',
},
'3': {
name: 'EVA-8',
},
},
'6': { # 空投武器
'1': {
name: '克雷贝尔狙击枪',
},
'2': {
name: '敖犬霰弹枪',
},
'3': {
name: '波塞克',
},
'4': {
name: '暴走',
},
}
}
toolkit.py
import mss # pip install mss
import ctypes
from ctypes import CDLL
import cfg
from cfg import detect, weapon
# 全局 dll
user32 = ctypes.windll.user32
gdi32 = ctypes.windll.gdi32
hdc = user32.GetDC(None)
try:
driver = CDLL(r'mouse.device.lgs.dll') # 在Python的string前面加上‘r’, 是为了告诉编译器这个string是个raw string(原始字符串),不要转义backslash(反斜杠) '\'
ok = driver.device_open() == 1
if not ok:
print('初始化失败, 未安装lgs/ghub驱动')
except FileNotFoundError:
print('初始化失败, 缺少文件')
class Mouse:
@staticmethod
def point():
return user32.GetCursorPos()
@staticmethod
def move(x, y, absolute=False):
if ok:
mx, my = x, y
if absolute:
ox, oy = user32.GetCursorPos()
mx = x - ox
my = y - oy
driver.moveR(mx, my, True)
@staticmethod
def moveHumanoid(x, y, absolute=False):
"""
仿真移动(还没做好)
"""
if ok:
ox, oy = user32.GetCursorPos() # 原鼠标位置
mx, my = x, y # 相对移动距离
if absolute:
mx = x - ox
my = y - oy
tx, ty = ox + mx, oy + my
print(f'({ox},{oy}), ({tx},{ty}), x:{mx},y:{my}')
# 以绝对位置方式移动(防止相对位置丢失精度)
adx, ady = abs(mx), abs(my)
if adx <= ady:
# 水平方向移动的距离短
for i in range(1, adx):
ix = i if mx > 0 else -i
temp = int(ady / adx * abs(ix))
iy = temp if my > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
else:
# 垂直方向移动的距离短
for i in range(1, ady):
iy = i if my > 0 else -i
temp = int(adx / ady * abs(iy))
ix = temp if mx > 0 else -temp
Mouse.move(ox + ix, oy + iy, absolute=True)
# time.sleep(0.001)
@staticmethod
def down(code):
if ok:
driver.mouse_down(code)
@staticmethod
def up(code):
if ok:
driver.mouse_up(code)
@staticmethod
def click(code):
"""
:param code: 1:左键, 2:中键, 3:右键, 4:侧下键, 5:侧上键, 6:DPI键
:return:
"""
if ok:
driver.mouse_down(code)
driver.mouse_up(code)
class Keyboard:
@staticmethod
def press(code):
if ok:
driver.key_down(code)
@staticmethod
def release(code):
if ok:
driver.key_up(code)
@staticmethod
def click(code):
"""
键盘按键函数中,传入的参数采用的是键盘按键对应的键码
:param code: 'a'-'z':A键-Z键, '0'-'9':0-9, 其他的没猜出来
:return:
"""
if ok:
driver.key_down(code)
driver.key_up(code)
class Monitor:
"""
显示器
"""
sct = mss.mss()
@staticmethod
def grab(region):
"""
region: tuple, (left, top, width, height)
pip install mss
"""
left, top, width, height = region
return Monitor.sct.grab(monitor={'left': left, 'top': top, 'width': width, 'height': height})
@staticmethod
def pixel(x, y):
"""
效率很低且不稳定, 单点检测都要耗时1-10ms
获取颜色, COLORREF 格式, 0x00FFFFFF
结果是int,
可以通过 print(hex(color)) 查看十六进制值
可以通过 print(color == 0x00FFFFFF) 进行颜色判断
"""
# hdc = user32.GetDC(None)
return gdi32.GetPixel(hdc, x, y)
class Resolution:
"""
分辨率
"""
@staticmethod
def display():
"""
显示分辨率
"""
w = user32.GetSystemMetrics(0)
h = user32.GetSystemMetrics(1)
return w, h
@staticmethod
def virtual():
"""
多屏幕组合的虚拟显示器分辨率
"""
w = user32.GetSystemMetrics(78)
h = user32.GetSystemMetrics(79)
return w, h
@staticmethod
def physical():
"""
物理分辨率
"""
# hdc = user32.GetDC(None)
w = gdi32.GetDeviceCaps(hdc, 118)
h = gdi32.GetDeviceCaps(hdc, 117)
return w, h
class Game:
"""
游戏工具
"""
@staticmethod
def game():
"""
是否在游戏内
太耗时了, 所以不能调的多了
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.game)
for item in data:
x, y = item.get(cfg.point)
if Monitor.pixel(x, y) != item.get(cfg.color):
return False
return True
@staticmethod
def index():
"""
武器索引和子弹类型索引
:return: 武器位索引, 1:1号位, 2:2号位, None:无武器, 拳头(这个暂时无法判断)
子弹类型索引, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投, None:无武器
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.pack)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
if data.get(cfg.color) == color:
return None, None
else:
bullet = data.get(hex(color))
return (1, bullet) if color == Monitor.pixel(x, y + 1) else (2, bullet)
@staticmethod
def weapon(index, bullet):
"""
通过武器位和子弹类型识别武器, 参考:config.detect.name
:param index: 武器位, 1:1号位, 2:2号位
:param bullet: 子弹类型, 1:轻型, 2:重型, 3:能量, 4:狙击, 5:霰弹, 6:空投
:return:
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.name)
color = data.get(cfg.color)
if index == 1:
lst = data.get(str(index)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x, y):
return i + 1
elif index == 2:
differ = data.get(str(index)).get(cfg.differ)
lst = data.get(str(1)).get(str(bullet))
for i in range(len(lst)):
x, y = lst[i]
if color == Monitor.pixel(x + differ, y):
return i + 1
return None
@staticmethod
def mode():
"""
武器模式
:return: 1:全自动, 2:半自动, None:其他
"""
w, h = Monitor.Resolution.display()
data = detect.get(f'{w}:{h}').get(cfg.mode)
x, y = data.get(cfg.point)
color = Monitor.pixel(x, y)
return data.get(hex(color))
@staticmethod
def detect():
"""
决策是否需要压枪, 向信号量写数据
"""
if Game.game() is False:
print('not in game')
return
index, bullet = Game.index()
if (index is None) | (bullet is None):
print('no weapon')
return
if Game.mode() is None:
print('not in full auto or semi auto mode')
return
arms = Game.weapon(index, bullet)
if arms is None:
print('detect weapon failure')
return
# 检测通过, 需要压枪
print(weapon.get(str(bullet)).get(str(arms)).get(cfg.name))
return weapon.get(str(bullet)).get(str(arms)).get(cfg.name)
apex.py
import time
import pynput # conda install pynput
import toolkit
ExitFlag = False
def down(x, y, button, pressed):
global ExitFlag
if ExitFlag:
print(ExitFlag)
return False # 结束监听线程
if pressed: # 按下
if pynput.mouse.Button.right == button:
toolkit.Game.detect()
mouseListener = pynput.mouse.Listener(on_click=down)
mouseListener.start()
def release(key):
if key == pynput.keyboard.Key.end:
print('end')
global ExitFlag
ExitFlag = True
return False
if key == pynput.keyboard.KeyCode.from_char('1'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('2'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('3'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('e'):
toolkit.Game.detect()
elif key == pynput.keyboard.KeyCode.from_char('v'):
toolkit.Game.detect()
keyboardListener = pynput.keyboard.Listener(on_release=release)
keyboardListener.start()
keyboardListener.join()
第二阶段实现 能自动识别出所有武器并采用对应抖枪参数执行压枪 (进行中)
第三阶段实现 放弃抖枪术 转常规后座抵消法 (未开始)
第四阶段实现 AI 目标检测, 移动鼠标, 彻底告别压枪 (未开始)