1.引言
扫雷作为家喻户晓的Windows游戏,作为经典游戏被广泛娱乐,那么能否做一个自动扫雷的小程序光速通关呢?答案是可以的。甚至可以做到无能能敌的地步,先上图,将记录设置为-8s,因此不存在有人能够突破这个记录了
2.实现方法
1. 实现逻辑
1)创建 WIN32 空项目;
2)打开扫雷进程,根据雷区的行和列所在内存地址,获取行、列的值;
3)根据雷区首地址和雷区范围,读取雷区数据;
4)将扫雷游戏界面置顶,读取雷区第i行第j列的数值,判断是否为地雷,如果不是则模拟鼠标左键单击操作,如果是地雷则模拟鼠标右键单击操作。
5)根据扫雷游戏界面的客户坐标系,计算第i行第j列的坐标位置,然后将鼠标移动到该位置,执行第3步的鼠标操作。
6)重复 3-4,直至雷区数据全部被扫描完成。
2. 相关API
读写内存地址中的内容使用的 WindowsAPI 函数为:
BOOL ReadProcessMemory(HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesRead);
BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPCVOID lpBuffer, SIZE_T nSize, SIZE_T *lpNumberOfBytesWritten);
读写内存 API 函数需要目标进程的句柄作为参数,为此需要调用 OpenProcess 用于获取进程句柄:
HANDLE OpenProcess(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwProcessId);
OpenProcess 需要进程 ID 作为参数,可通过 GetWindowThreadProcessId 函
数得到:
DWORD GetWindowThreadProcessId(HWND hWnd, LPDWORD lpdwProcessId);
GetWindowThreadProcessId 函数需要窗口句柄作为参数,为此可通过 FindWindow 获得,而该 API 函数用到的窗口类和窗口标题名两个参数,已在步骤7中通过 Spy++得到
HWND FindWindowA(LPCSTR lpClassName, LPCSTR lpWindowName);
模拟鼠标操作的 API 函数为 mouse_event,具体使用方式为,使用SetCursorPos API 函数将鼠标移动到指定位置,然后模拟鼠标按下和弹起操作。
函数声明如下:
void mouse_event(DWORD dwFlags, DWORD dx, DWORD dy, DWORD dwData, ULONG_PTR dwExtraInfo);
BOOL SetCursorPos(int X, int Y);
本实验中 mouse_event 函数仅需设置第一项参数即可,具体包括左键按下、弹起(MOUSEEVENTF_LEFTDOWN、MOUSEEVENTF_LEFTUP),右键按下、弹起(MOUSEEVENTF_RIGHTDOWN、MOUSEEVENTF_RIGHTUP)。
将某个 GUI 程序界面置顶的 API 函数为:
BOOL SetForegroundWindow(HWND hWnd);
3.脚本实现
确定扫雷游戏格子大小
打开 Spy++,单击“监视--日志消息”,勾选“隐藏 Spy++选项”,拖动“查找程序工具”至扫雷游戏主界面,如图 8-4 所示,显示的内容包括扫雷窗口类(WNDCLASS)的名称以及标题名。
切换至Spy++的“消息”窗口,先清除所有消息,然后勾选 “WM_LBUTTONDOWN 和 WM_LBUTTONUP”两个消息,如图 8-5 所示,单击确定后返回 Spy++主界面。
返回扫雷游戏界面,依次单击左上角、紧邻左上角右方、紧邻左上角下方的三个格子,查看 Spy++记录的鼠标单击时的 xPos 和 yPos 的值,以此计算每个格子的宽度和高度。
首个雷区坐标为:
xPos =14
yPos =58
在Spy++界面,右键--清除消息日志,然后重复上述操作,也可间隔数个格 子单击,以此计算每个格子的平均值,作为每个格子的宽和高度值。格子的宽度为:16 ;格子的高度为:16 。
#include <windows.h>
int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
const char* text = "扫雷";
HWND hwnd = FindWindow(text, text);
if (hwnd == NULL)
{
MessageBox(NULL, TEXT("Winmine is not found!"), TEXT("AutoMineSweeper"), MB_ICONSTOP);
return 1;
}
DWORD pid;
GetWindowThreadProcessId(hwnd, &pid);
HANDLE handle = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pid);
int width, height;
width = -1;
height = -1;
ReadProcessMemory(handle, (LPVOID)0x01005334, &width, 4, NULL);
ReadProcessMemory(handle, (LPVOID)0x01005338, &height, 4, NULL);
/*BYTE **data;
data=(BYTE**)malloc(32 * height * sizeof(BYTE));*/
//BYTE data[] = new BYTE(32 * height);
BYTE data[32*32] = { 0 };
ReadProcessMemory(handle, (LPVOID)0x01005361, &data, 32 * height, NULL);
ShowWindow(hwnd, SW_RESTORE);
SetForegroundWindow(hwnd);
Sleep(300);
int width_num = 32;
int x_pos = 14; //雷区首个格子的位置
int y_pos = 58;
RECT rc;
BOOL bRet = GetClientRect(hwnd, &rc);
//更改扫雷时间,将其设置为-8
int time = -8;
WriteProcessMemory(handle, (LPVOID)0x0100579C, &time, 4, NULL);
for (int y = 1; y <= height; y++)
{
for (int x = 1; x <= width; x++)
{
UINT downMsg = x_pos, upMsg = y_pos;
if (data[width_num * (x - 1) + (y - 1)] == 0x10)
break;
else
{
if (data[(x - 1) + width_num * (y - 1)] == 0x0F)
{
downMsg = MOUSEEVENTF_LEFTDOWN;
upMsg = MOUSEEVENTF_LEFTUP;
}
else
{
downMsg = MOUSEEVENTF_RIGHTDOWN;
upMsg = MOUSEEVENTF_RIGHTUP;
}
}
POINT curPos = { rc.left + x_pos + (x - 1) * 16, rc.top + y_pos + (y - 1) * 16 };
ClientToScreen(hwnd, &curPos);
SetCursorPos(curPos.x, curPos.y);
mouse_event(downMsg, 0, 0, 0, 0);
mouse_event(upMsg, 0, 0, 0, 0);
Sleep(1);
}
}
CloseHandle(handle);
return 0;
}