QQ连连看辅助实现

就在前几天室友的电脑系统出问题了,打不了LOL。于是就玩连连看,被人虐了之后。我决定帮他写个辅助。

Tools

CE 6.5
OllyDbg
Visual Studio 2017

Environment

Windows 10专业版 64位 1803

辅助要实现的功能是自动消除一组,具体可分解为:读取当前地图、找到一组可消除的组、模拟点击消除。

地图的读取

连连看的地图大小为11*19。
读取地图可以有两种方式,图片识别与内存读取。

图片识别

连连看的窗口长这样,窗口大小是800*600。
左上角方块 的像素坐标为(14,181),每个方块的大小为31*35。可以算出最右下角坐标为(14+31*18,181+35*10)=(572,531)。经验证,右下角的方块坐标确实为(572,531)。所以,我们可以通过计算得到每一个方块的像素坐标。读取对应的像素,建立地图。

内存读取

使用内存读取,首先要找到储存地图的地址。根据直觉,这是使用一个二维数组进行的存储。而左上角就是数组开始的地址。由于方块的种类的种数不多,使用单字节存储即可。
所以CE的Value Type选择Byte,而现在我们不知道每个方块所对应的值,所以Scan Type选择”Changed value”与”Unchanged value”搜索。当左上角的方块改变就使用”Changed value”未改变时使用”Unchanged value”。最终找到了左上角方块的地址。

查看这附近的内存 ,果然和地图完全吻合。
读取内存,用到的windows函数是ReadProcessMemory
函数原型如下:

1
2
3
4
5
6
7
BOOL ReadProcessMemory(
HANDLE hProcess,
LPCVOID lpBaseAddress,
LPVOID lpBuffer,
SIZE_T nSize,
SIZE_T *lpNumberOfBytesRead
);
  • hProcess
    要读取内存的进程句柄。句柄必须具有PROCESS_VM_READ权限。
  • lpBaseAddress
    指向要读取的进程的地址的指针,也就是刚刚找到的0x00199F68
  • lpBuffer
    指向缓冲区的指针,该缓冲区从指定进程的地址空间接受内容。
  • nSize
    从指定进程读取的字节数。
  • lpNumberOfBytesRead
    指向变量的指针,该变量接受传输到指定缓冲区的字节数。

通过这个函数,就可以直接读取到地图。

可消除判断

拿到了地图找到两个相同方块后,就需要进行可消除判断。连连看的消除逻辑分为直接连接、一折连接、二折连接。而一折连接是两个直接连接的组合,二折连接是一个一折连接和一个直接连接的组合。

直接连接消除判断

直接连接的前提条件是两个方块的X坐标或Y坐标相等。判断条件是两个方块之间没有方块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
bool horizon(Point p1, Point p2) //进行水平反向的连接判断
{
if (p1==p2) //如果两个方块为同一个方块,则返回false
return false;
if (p1.getX() != p2.getX()) //如果两个方块的X坐标不相等,则返回false
return false;
int start_y = min(p1.getY(), p2.getY())+1;
int end_y = max(p1.getY(), p2.getY());
for (int i = start_y; i < end_y; i++)
if (map.isBlock(Point(p1.getX(),i))) //两方块之间找到任何一个方块,则返回false
return false;
return true;
}
bool vertical(Point p1, Point p2) //同上,进行竖直方向的连接判断
{
if (p1==p2)
return false;
if (p1.getY() != p2.getY())
return false;
int start_x = min(p1.getX(), p2.getX())+1;
int end_x = max(p1.getX(), p2.getX());
for (int i = start_x; i < end_x; i++)
if (map.isBlock(Point(i,p1.getY())))
return false;
return true;
}

一折连接消除判断

一折连接需要找到一个中介点,使得起始点和中介点可以进行水平连接(竖直连接),中介点和终点可以进行竖直连接(水平连接)。
如果起始点与终点为p1(x,y),p2(x,y),那么中介点就应该是(p1.x,p2.y)或(p2.x,p1.y)。

1
2
3
4
5
6
7
8
9
10
11
12
13
bool turn_once(Point p1, Point p2)
{
if (p1==p2)
return false;
Point c(p1.getX(), p2.getY()); //中介点1
Point d(p2.getX(), p1.getY()); //中介点2
bool ret = false;
if (!map.isBlock(c))
ret |= horizon(p1, c) && vertical(c, p2);
if (!map.isBlock(d))
ret |= horizon(p1, d) && vertical(d, p2);
return ret;
}

二折连接消除判断

二折连接需要找到一个中介点,使得起始点到中介点可以一折连接,中介点到终点可以进行直接连接或者起始点到中介点可以直接连接,中介点到终点可以进行一折连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
bool turn_twice(Point p1, Point p2)
{
if (p1 == p2)
return false;
for (int i = 0; i < 11; i++)
{
for (int j = 0; j < 19; j++)
{
Point temp(i, j);
if (i != p1.getX() && i != p2.getX() && j != p1.getY() && j != p2.getY()) //如果中介点和起始点和终点的XY轴都不相同则略过
continue;
if ((temp == p1) || (temp == p2)) //如果中介点为起始点或终点则略过
continue;
if (map.isBlock(Point(i,j))) //如果中介点为方块则略过
continue;
if (turn_once(p1, temp) && (horizon(temp, p2) || vertical(temp, p2))) //终点和中介点一折连接,中介点和起始点直接连接
return true;
if (turn_once(temp, p2) && (horizon(p1, temp) || vertical(p1, temp)))//起始点和中介点一折连接,中介点和终点直接连接
return true;
}
}
return false;
}

总和

将以上的直接连接、一折连接、二折连接代码进行一下封装

1
2
3
4
5
6
7
8
9
10
11
12
bool isRemove(Point p1, Point p2)
{
if (horizon(p1, p2))
return true;
if (vertical(p1, p2))
return true;
if (turn_once(p1, p2))
return true;
if (turn_twice(p1, p2))
return true;
return false;
}

模拟点击

在windows中,点击是一个事件,而Windows把这个事件翻译为消息。所以我们要给连连看模拟单击,即给窗体发送鼠标左键按下和鼠标左键弹起消息。
这时候就需要Windows函数PostMessage
函数原型如下:

1
2
3
4
5
6
BOOL PostMessageA(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);

  • hWnd
    接受消息的窗口句柄
  • Msg
    要发送的消息,具体可以参考微软提供的文档,这里我们要使用的是WM_LBUTTONDOWN与WM_LBUTTONUP,左键按下和左键弹起
  • wParam
    用于特定消息的其余信息
  • lParam
    用于特定消息的其余信息,在文档中可以得到,在单击这个事件分解的消息中,wParam用MK_LBUTTON,lParam的低字节对应x坐标,高字节对应y坐标。

至此还需要的是把地图中的坐标映射成像素坐标。而由坐标与像素坐标的规律,可以得到映射函数(32+31*p.y,198+35*p.x);

1
2
3
4
5
6
7
8
9
10
Point reflact(Point p) //映射函数
{
return Point(32 + 31 * p.getY(), 198 + 35 * p.getX());
}
void click(Point p)
{
Point px = reflact(p);
PostMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(px.getX(), px.getY())); //发送鼠标左键按下消息
PostMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELPARAM(px.getX(), px.getY())); //发送鼠标左键弹起消息
}

总代码

最后是所有代码的总和,可以直接扔到VS2017编译

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
#include <iostream>
#include <Windows.h>
#include <TlHelp32.h>
using namespace std;

HANDLE hProcess;
HWND hWnd;

class Point
{
private:
int x, y;
public:
Point(int x1, int y1)
{
x = x1, y = y1;
}
int getX() { return x; }
int getY() { return y; }
void setX(int x1) { x = x1; }
void setY(int y1) { y = y1; }
bool operator == (const Point &b) const
{
return this->x == b.x && this->y == b.y;
}
bool operator != (const Point &b) const
{
return !((*this) == b);
}
};
class Map
{
private:
unsigned char map[11][19];
public:
Map()
{
for (int i = 0; i < 11; i++)
for (int j = 0; j < 19; j++)
map[i][j] = 0;
}
unsigned char * getBuffer()
{
return (unsigned char *)map;
}
bool isBlock(Point p)
{
if (map[p.getX()][p.getY()] == 0) return false;
return true;
}
unsigned char getBlock(Point p)
{
return map[p.getX()][p.getY()];
}
void setBlock(Point p, unsigned char value)
{
map[p.getX()][p.getY()] = value;
}
void print()
{
for (int i = 0; i < 11; i++)
{
for (int j = 0; j < 19; j++)
printf_s("%x ", map[i][j]);
printf_s("\n");
}
}
};
Map map;
bool horizon(Point p1, Point p2)
{
if (p1==p2)
return false;
if (p1.getX() != p2.getX())
return false;
int start_y = min(p1.getY(), p2.getY())+1;
int end_y = max(p1.getY(), p2.getY());
for (int i = start_y; i < end_y; i++)
if (map.isBlock(Point(p1.getX(),i)))
return false;
return true;
}
bool vertical(Point p1, Point p2)
{
if (p1==p2)
return false;
if (p1.getY() != p2.getY())
return false;
int start_x = min(p1.getX(), p2.getX())+1;
int end_x = max(p1.getX(), p2.getX());
for (int i = start_x; i < end_x; i++)
if (map.isBlock(Point(i,p1.getY())))
return false;
return true;
}

bool turn_once(Point p1, Point p2)
{
if (p1==p2)
return false;
Point c(p1.getX(), p2.getY());
Point d(p2.getX(), p1.getY());
bool ret = false;
if (!map.isBlock(c))
ret |= horizon(p1, c) && vertical(c, p2);
if (!map.isBlock(d))
ret |= horizon(p1, d) && vertical(d, p2);
return ret;
}
bool turn_twice(Point p1, Point p2)
{
if (p1 == p2)
return false;
for (int i = 0; i < 11; i++)
{
for (int j = 0; j < 19; j++)
{
Point temp(i, j);
if (i != p1.getX() && i != p2.getX() && j != p1.getY() && j != p2.getY())
continue;
if ((temp == p1) || (temp == p2))
continue;
if (map.isBlock(Point(i,j)))
continue;
if (turn_once(p1, temp) && (horizon(temp, p2) || vertical(temp, p2)))
return true;
if (turn_once(temp, p2) && (horizon(p1, temp) || vertical(p1, temp)))
return true;
}
}
return false;
}
bool isRemove(Point p1, Point p2)
{
if (horizon(p1, p2))
return true;
if (vertical(p1, p2))
return true;
if (turn_once(p1, p2))
return true;
if (turn_twice(p1, p2))
return true;
return false;
}
bool readMemory()
{

SIZE_T size;
if (ReadProcessMemory(hProcess, (LPVOID)0x00199F68, map.getBuffer(), 19 * 11, &size) == 0)
{
printf_s("%x", GetLastError());
printf_s("读取失败!!\n");
return false;
}
return true;
}

Point reflact(Point p)
{
return Point(32 + 31 * p.getY(), 198 + 35 * p.getX());
}


void click(Point p)
{
Point px = reflact(p);
PostMessage(hWnd, WM_LBUTTONDOWN, MK_LBUTTON, MAKELPARAM(px.getX(), px.getY()));
PostMessage(hWnd, WM_LBUTTONUP, MK_LBUTTON, MAKELPARAM(px.getX(), px.getY()));
}

void removeOne()
{
if (!readMemory()) return;
try
{
for (int i1 = 0; i1 < 11; i1++)
for (int j1 = 0; j1 < 19; j1++)
if (map.getBlock(Point(i1, j1)) != 0)
for (int i2 = 0; i2 < 11; i2++)
for (int j2 = 0; j2 < 19; j2++)
if (map.getBlock(Point(i1, j1)) == map.getBlock(Point(i2, j2)) && Point(i1, j1) != Point(i2, j2))
if (isRemove(Point(i1, j1), Point(i2, j2)))
{
printf_s("(%d,%d),(%d,%d)\n", i1, j1, i2, j2);
click(Point(i1, j1));
click(Point(i2, j2));
throw exception(); //使用异常机制,优雅的跳出多层循环。
}
}
catch (exception e)
{

}
}

void process()
{
DWORD PID = 0;
HANDLE hProcessSnap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
PROCESSENTRY32 pe = { sizeof(PROCESSENTRY32) };
Process32First(hProcessSnap, &pe);
do
{
int pSize = WideCharToMultiByte(CP_OEMCP, 0, pe.szExeFile, wcslen(pe.szExeFile), NULL, 0, NULL, NULL);
char* pCStrKey = new char[pSize + 1];
WideCharToMultiByte(CP_OEMCP, 0, pe.szExeFile, wcslen(pe.szExeFile), pCStrKey, pSize, NULL, NULL);
pCStrKey[pSize] = '\0';
if (!strcmp(pCStrKey, "kyodaiRPG.exe"))
{
PID = pe.th32ProcessID;
}
} while (Process32Next(hProcessSnap, &pe));
CloseHandle(hProcessSnap);
if (PID == 0)
{
printf_s("无法获取连连看的PID,请先进入游戏!\n");
return;
}
hWnd = FindWindowA(NULL, "QQ游戏 - 连连看角色版");
printf_s("kyodaiRPG.exe PID为:%d\n", PID);
printf_s("QQ游戏 - 连连看角色版 窗口句柄为:%d\n", hWnd);

hProcess = OpenProcess(PROCESS_ALL_ACCESS, NULL, PID);
if (hProcess == NULL)
{
printf_s("打开进程失败!请用管理员身份运行!\n");
return;
}
printf_s("打开进程成功!\n");
printf_s("======================\n");
printf_s("按任意键进行消除\n");
printf_s("Q:退出\n");
char c;
while (1)
{
scanf_s("%c", &c);
if (c == 'q')
{
CloseHandle(hProcess);
break;
}
else
{
removeOne();
}
}
system("cls");
}

int main()
{
int i;
while (1)
{
printf_s("1:进入PID检测\n");
printf_s("2:退出\n");
scanf_s("%d", &i);
system("cls");
if (i == 1)
{
process();
}
else if (i == 2)
{
break;
}
}
return 0;
}

文章目录
  1. 1. 地图的读取
    1. 1.1. 图片识别
    2. 1.2. 内存读取
  2. 2. 可消除判断
    1. 2.1. 直接连接消除判断
    2. 2.2. 一折连接消除判断
    3. 2.3. 二折连接消除判断
    4. 2.4. 总和
  3. 3. 模拟点击
  4. 4. 总代码