【Python自动化】PyAutoGUI在游戏测试中的应用

一、前言

在日常游戏的强化学习稳定性测试自动跑测中,发现了一些机器在长时间挂机后的崩溃现象,导致游戏进程退回桌面,或在匹配界面挂机。此时可以借助PyAutoGUI库实现自动点击开始游戏并重新匹配。

二、准备所需库

实现自动点击重开,需要监控游戏进程,需要psutil及subprocess库;实现图像自动化,需要pyautogui库;获取设备基本硬件信息,需要GPUtil、cpuinfo及socket库。如还需图像自动化的扩展性,还需用到opencv-python(即cv2)。

三、准备脚本

这里思路为,监控游戏开启时的PID,如PID为空,则重新启动游戏;其中的fight.png、start_lobby.png、start_match.png、windows.png均为预先截图,存放在脚本目录中。

这里我的全屏检测思路为检测windows10开始按钮,如存在,则判断为非全屏状态,反之则是。

这里需要注意的是,推荐使用win32库收集电脑硬件信息,其他第三方库使用可能会造成打包后的exe文件运行卡住导致后续消息信息无法发送。

import pyautogui
import subprocess
import time
import psutil
import requests
import win32com.client
import socket
import json
from py3nvml.py3nvml import *

# 任务栏游戏图标检测
def game_icon_check():
    try:
        pyautogui.locateCenterOnScreen('images\game_icon.png', confidence = 0.8)
    except Exception:
        return False
    return True

# windows开始图标检测
def is_win_icon_exsist():
    try:
        pyautogui.locateCenterOnScreen('images\win_check.png', confidence = 0.8)
    except Exception:
        return False
    return True

# 以每次游戏进程启动的PID作为检测进程是否存在的条件
def is_process_running(pid):
    return psutil.pid_exists(pid)

# 通过pid来kill后台进程
def kill_process_by_pid(process_name):
    for proc in psutil.process_iter(['pid']):
        if proc.info['pid'] == pid:
            try:
                proc.kill()
                time.sleep(1)
                print(f'此进程{process_name}-{proc.info["pid"]}已关闭。')
            except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
                print(f'此进程{process_name}-{proc.info["pid"]}不存在。')

# 全屏使用windows开始图标和游戏任务栏图标检查(进入大厅也放在这一步)
def fullscreen_check():
    try:
        loc1 = pyautogui.locateCenterOnScreen('images\win_check.png')
        loc2 = pyautogui.locateCenterOnScreen('images\game_icon.png')
        if (loc1 is not None) and (loc2 is not None):
            print('游戏不为全屏,切换为全屏......')
            pyautogui.hotkey('alt', 'enter')  
        time.sleep(3)
    except Exception:
        print('游戏为全屏')
        time.sleep(3)      

    while True:
        try:
            loc = pyautogui.locateCenterOnScreen('images\start_lobby.png', confidence = 0.8) 
            pyautogui.click(loc, clicks = 1, interval = 0.5, duration = 1)
            if loc is not None:
                break
        except Exception:
            print('大厅连接中,请稍后......')
            time.sleep(5)

def click_to_start():
    while True:
        # 如有断线重连提示,点击确认重新加入
        try:
            # 设置建议进入大厅时间等待可以久一点
            time.sleep(10)
            loc1 = pyautogui.locateCenterOnScreen('images\\reconnet.png', confidence = 0.8)
            if loc1 is not None:
                loc2 = pyautogui.locateCenterOnScreen('images\confirm_button.png', confidence = 0.6)
                pyautogui.click(loc2, clicks = 1, interval = 0.5, duration = 1)
                break
        except Exception:
            print('无需重连') 

        try:
            for step in steps:
                loc3 = pyautogui.locateCenterOnScreen(step, confidence = 0.8) 
                print(f'点击【{step}】')
                if loc3 is not None:
                    time.sleep(1)
                    pyautogui.click(loc3, clicks = 1, interval = 0.5, duration = 1)
        except Exception:
            print('未找到按钮,请检查主屏幕显示!')
        break
    print('进入对局中......')

def start_process(process_name):
    global pid
    global cnt
    
    proc = subprocess.Popen(process_name, cwd = 'C:\\Windows\\System32') 
    pid = proc.pid
    print(f'游戏开启,版本为【{this_time_version}服】-【{process_name}】-【{pid}】')
    time.sleep(10)
    cnt += 1

def notice():
    global CPU
    global GPU
    global IP

    try:
        # CPU信息
        for cpu in wmi.InstancesOf("Win32_Processor"):  
            CPU = cpu.Name

        # 显卡信息
        for gpu in wmi.InstancesOf("Win32_VideoController"):
            GPU = gpu.Name

        # 内存信息
        for mem in wmi.InstancesOf("Win32_PhysicalMemory"):
            RAM = f'{int(mem.Capacity) / 1024**3:.0f}'

        # 内网IP地址
        IP = socket.gethostbyname(socket.gethostname())

        headers = {"Content-Type": "application/json; charset = utf-8"}
        message = {
            "msgtype": "markdown",
            "markdown": {
                "content": f"<font color=\"warning\">【稳定性测试中此机型游戏进程因崩溃或其他原因退出多次,请相关同学注意!】</font>\n"
                            f"> <font color=\"info\">CPU:{CPU}</font>\n"
                            f"> <font color=\"info\">显卡:{GPU}</font>\n"
                            f"> <font color=\"info\">内存:{RAM}GB</font>\n"
                            f"> <font color=\"info\">内网IP地址:{IP}</font>\n"
                            f"> <font color=\"info\">版本:{this_time_version}</font>\n"
                            f"> <font color=\"info\">Crash-Report:[点击查看]({crash_report_url})</font>\n"
            }
        }
        requests.post(notice_url, headers = headers, json = message)
    except Exception:
        print('硬件信息收集失败!')

# 上传客户端崩溃日志
def upload_log():
    # 进入客户端日志目录将最近崩溃日志拷贝至公盘61.175的崩溃日志汇总内
    command = f'New-Item -path {pub_crash_dir}\{CPU}-{GPU}-{IP} -itemtype directory -force; cd {log_dir}; Get-ChildItem -Path .\*.log | Sort-Object LastWritetime -Descending | Select-Object -First {restart_time} | Copy-Item -Destination {pub_crash_dir}\{CPU}-{GPU}'
    subprocess.run(['powershell', '-Command', command], capture_output=True, text=True)

# 未知启动问题,暂时先通过拷贝指定dll文件到游戏目录下解决
def copy_dll():
    command = f'Copy-Item -Path .\\images\\msvcp140_2.dll, .\\images\\vcruntime140_1.dll -Destination {install_dir}\\ -Force'
    subprocess.run(['powershell', '-Command', command], capture_output=True, text=True)    

if __name__ == '__main__':
    wmi = win32com.client.GetObject("winmgmts:")
    notice_url= '******************************'
    crash_report_url = '***********************'
    dgs_dir = r'*************\*********\******.json'
    steps = ['images\\fight.png', 'images\\start_match.png']
    pub_crash_dir = '*************'
    cnt = 0

    # 初始化参数
    with open('config.json', 'r', encoding = 'utf-8') as t:
        this_time = json.load(t)    
        this_time_version = this_time.get('Version')
        restart_time = int(this_time.get('RT'))
        wait_time = int(this_time.get('WT'))
        params = this_time.get('Params')

    # 读取对应DGS目录json文件以获取游戏安装目录并启动
    with open(dgs_dir, 'r', encoding = 'utf-8') as f:
        configs = json.load(f)
    for config in configs:
        if config['GameVersionName'] == this_time_version:
            install_dir = config['InstallDirectory']
            break

    process_name = f'{install_dir}\*****.exe'
    log_dir = f'{install_dir}'

    copy_dll()

    while True:
        # 首次开始游戏
        if cnt == 0:
            start_process(process_name)
            fullscreen_check()
            click_to_start()

        # 进程未运行且能检测到windows图标,判断为游戏进程结束
        if (not is_process_running(pid)) and is_win_icon_exsist():
            print('游戏意外退出,进程重启中......当前时间:', time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
            if cnt == restart_time:
                print(f'严重问题!取消重启!崩溃次数:【{cnt}次】 当前时间:', time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
                kill_process_by_pid(process_name)
                notice()
                upload_log()
                break
            start_process(process_name)
            fullscreen_check()
            click_to_start()
        # 退到桌面可以检测到windows图标但进程仍然存在,仍判断为游戏进程结束
        elif is_process_running(pid) and is_win_icon_exsist() and not game_icon_check():
            # 等待尽可能长让进程自行销毁
            time.sleep(30)
            print('游戏意外退出,进程重启中......当前时间:', time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
            if cnt == restart_time:
                print(f'严重问题!取消重启!崩溃次数:【{cnt}次】 当前时间:', time.strftime('%Y-%m-%d %H:%M:%S',time.localtime(time.time())))
                kill_process_by_pid(process_name)
                notice()
                upload_log()
                break
            kill_process_by_pid(process_name)
            time.sleep(1) 
            start_process(process_name)
            fullscreen_check()
            click_to_start()

        # 检测游戏对局是否结束,如结束重新点击进行匹配
        try:
            loc1 = pyautogui.locateCenterOnScreen('images\\fight.png', confidence = 0.8)
            # 检测正常对局结束退到大厅后短暂等待10s
            time.sleep(10)
            pyautogui.click(loc1, clicks = 1, interval = 0.5, duration = 1)
            time.sleep(3)
            loc2 = pyautogui.locateCenterOnScreen('images\start_match.png', confidence = 0.8)
            pyautogui.click(loc2, clicks = 1, interval = 0.5, duration = 1)
            print('重新开始匹配......')
        except Exception:
            print('游戏对局进行中......')

        # 进程循环检测
        time.sleep(wait_time) 

四、使用Pyinstaller打包并发布

pyinstaller --onefile .\auto_restart.py

PS:可以编写一个Bat脚本去执行exe文件,在exe文件退出后,保持命令窗口可见,方便后续收集相关打印信息。(Bat脚本应与exe文件在同一目录下)

@echo off
path = .\;
auto_restart.exe
pause

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值