博文目录


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

环境准备

conda create -n apex python=3.9

操纵键鼠

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

罗技驱动分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()

键鼠监听

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

武器识别

Python Apex Legends 武器自动识别与压枪 全过程记录-LMLPHP

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

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

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

Python Apex Legends 武器自动识别与压枪 全过程记录-LMLPHP

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

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

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

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

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

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

Python Apex Legends 武器自动识别与压枪 全过程记录-LMLPHP
需要压枪的只有全自动和半自动两种模式的武器, 单发不需要压枪(后面有可能做自动单发, 到时候在考虑), 喷子和狙不需要压枪

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

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

何时触发识别

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

压枪思路

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

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

我先试试 抖枪大法

组织数据

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

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

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

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

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

cfg.py


mode = 'mode'
name = 'name'
game = 'game'
data = 'data'
pack = 'pack'  # 背包
color = 'color'
point = 'point'
index = 'index'
bullet = 'bullet'  # 子弹
differ = 'differ'
positive = 'positive'  # 肯定的
negative = 'negative'  # 否定的

# 检测数据
detect = {
    "3440:1440": {
        game: [  # 判断是否在游戏中
            {
                point: (236, 1344),  # 点的坐标, 血条左上角
                color: 0x00FFFFFF  # 点的颜色, 255, 255, 255
            },
            {
                point: (2692, 1372),  # 生存物品右下角
                color: 0x959595  # 149, 149, 149
            }
        ],
        pack: {  # 背包状态, 有无武器, 选择的武器
            point: (2900, 1372),  # 两把武器时, 1号武器上面边框分界线的上半部分, y+1 就是1号武器上面边框分界线的下半部分
            color: 0x808080,  # 无武器时, 灰色, 128, 128, 128
            '0x447bb4': 1,  # 轻型弹药武器, 子弹类型: 1/2/3/4/5/6/None(无武器)
            '0x839b54': 2,  # 重型弹药武器
            '0x3da084': 3,  # 能量弹药武器
            '0xce5f6e': 4,  # 狙击弹药武器
            '0xf339b': 5,  # 霰弹枪弹药武器
            '0x5302ff': 6,  # 空投武器
        },
        mode: {  # 武器模式, 全自动/半自动/单发/其他
            point: (3148, 1349),
            '0xf8f8f8': 1,  # 全自动
            '0xfefefe': 2  # 半自动
        },
        name: {  # 武器名称判断
            color: 0x00FFFFFF,
            '1': {  # 1号武器
                '1': [  # 轻型弹药武器
                    (2959, 1386),  # 1: RE-45 自动手枪
                    (2970, 1385),  # 2: 转换者冲锋枪
                    (2972, 1386),  # 3: R-301 卡宾枪
                    (2976, 1386),  # 4: R-99 冲锋枪
                    (2980, 1386),  # 5: P2020 手枪
                    (2980, 1384),  # 6: 喷火轻机枪
                    (2987, 1387),  # 7: G7 侦查枪
                    (3015, 1386),  # 8: CAR (轻型弹药)
                ],
                '2': [  # 重型弹药武器
                    (2957, 1385),  # 1: 赫姆洛克突击步枪
                    (2982, 1385),  # 2: 猎兽冲锋枪
                    (2990, 1393),  # 3: 平行步枪
                    (3004, 1386),  # 4: 30-30
                    (3015, 1386),  # 5: CAR (重型弹药)
                ],
                '3': [  # 能量弹药武器
                    (2955, 1386),  # 1: L-STAR能量机枪
                    (2970, 1384),  # 2: 三重式狙击枪
                    (2981, 1385),  # 3: 电能冲锋枪
                    (2986, 1384),  # 4: 专注轻机枪
                    (2980, 1384),  # 5: 哈沃克步枪
                ],
                '4': [  # 狙击弹药武器
                    (2969, 1395),  # 1: 哨兵狙击步枪
                    (2999, 1382),  # 2: 充能步枪
                    (2992, 1385),  # 3: 辅助手枪
                    (3016, 1383),  # 4: 长弓
                ],
                '5': [  # 霰弹枪弹药武器
                    (2957, 1384),  # 1: 和平捍卫者霰弹枪
                    (2995, 1382),  # 2: 莫桑比克
                    (3005, 1386),  # 3: EVA-8
                ],
                '6': [  # 空投武器
                    (2958, 1384),  # 1: 克雷贝尔狙击枪
                    (2983, 1384),  # 2: 敖犬霰弹枪
                    (3003, 1383),  # 3: 波塞克
                    (3014, 1383),  # 4: 暴走
                ]
            },
            '2': {
                differ: 195
            }
        }
    },
    "2560:1440": {

    },
    "2560:1080": {

    },
    "1920:1080": {

    }
}

# 武器数据
weapon = {
    '1': {  # 轻型弹药武器
        '1': {
            name: 'RE-45 自动手枪',
        },
        '2': {
            name: '转换者冲锋枪',
        },
        '3': {
            name: 'R-301 卡宾枪',
        },
        '4': {
            name: 'R-99 冲锋枪',
        },
        '5': {
            name: 'P2020 手枪',
        },
        '6': {
            name: '喷火轻机枪',
        },
        '7': {
            name: 'G7 侦查枪',
        },
        '8': {
            name: 'CAR (轻型弹药)',
        }
    },
    '2': {  # 重型弹药武器
        '1': {
            name: '赫姆洛克突击步枪',
        },
        '2': {
            name: '猎兽冲锋枪',
        },
        '3': {
            name: '平行步枪',
        },
        '4': {
            name: '30-30',
        },
        '5': {
            name: 'CAR (重型弹药)',
        }
    },
    '3': {  # 能量弹药武器
        '1': {
            name: 'L-STAR能量机枪',
        },
        '2': {
            name: '三重式狙击枪',
        },
        '3': {
            name: '电能冲锋枪',
        },
        '4': {
            name: '专注轻机枪',
        },
        '5': {
            name: '哈沃克步枪',
        },
    },
    '4': {  # 狙击弹药武器
        '1': {
            name: '哨兵狙击步枪',
        },
        '2': {
            name: '充能步枪',
        },
        '3': {
            name: '辅助手枪',
        },
        '4': {
            name: '长弓',
        },
    },
    '5': {  # 霰弹弹药武器
        '1': {
            name: '和平捍卫者霰弹枪',
        },
        '2': {
            name: '莫桑比克',
        },
        '3': {
            name: 'EVA-8',
        },
    },
    '6': {  # 空投武器
        '1': {
            name: '克雷贝尔狙击枪',
        },
        '2': {
            name: '敖犬霰弹枪',
        },
        '3': {
            name: '波塞克',
        },
        '4': {
            name: '暴走',
        },
    }
}

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

Python Apex Legends 武器自动识别与压枪 全过程记录-LMLPHP

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

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


09-19 14:30