一时兴起,决定写个连连看的外挂玩玩...于是断断续续经过几个晚上的努力基本呈现雏形...之前也看过一些外挂技术的文章,确实这里面的技术深不可测,第一次写就搞个简单的吧,以后再慢慢改进。
最简单的外挂莫过于器械式的,也就是通过界面分析,然后去模拟一些鼠标或者键盘动作。连连看外挂就可以通过这种方式去写。整个外挂基本分为以下几步完成:
1. 获取界面信息,当然最主要的是方格信息,有了这个其实足够写一个连连看外挂了,但是为了功能更强大,我另外获取了其他一些界面元素,比如其他玩家的速度(剩下的方格速),开始按钮的位置等等。
2. 连连看算法的实现,其实这个是比较简单的。基本可以分为两种:两条X连线+一条Y连线、两条Y连线+一条X连线。当然也包括了各种特殊情况。比如一条X一条Y等等。
3. 鼠标事件的模拟。
1. 获取界面信息
这部分工作其实是最繁琐的,只能依靠不断的调式去完成,我在这个过程使用到了系统钩子,在一定程度上简化了这个过程。首先要获得的是每个方格的信息,QQ连连看中一共有11×19个方格数,一共有44(好像45)种不同的图案,这个基本可以靠肉眼获得。
接下来的事情就需要代码去完成了,主要包括起始点,每个方格的长和宽,可以唯一区分每种图案的N个像素点。首先需要获得一个大概的数值,我写了一个系统的鼠标钩子,主要完成的功能是通过鼠标在QQ连连看窗口的点击,获得该点的client端坐标和颜色值。基本代码如下:
- // MouseHook.cpp : Defines the exported functions for the DLL application.
- //
- #include "stdafx.h"
- #pragma data_seg ("shareddata")
- HHOOK g_MouseHook = NULL;
- HINSTANCE g_Instance = NULL;
- HWND g_GameWnd = NULL;
- #pragma data_seg()
- #pragma comment(linker, "/SECTION:shareddata,RWS")
- int APIENTRY DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
- {
- UNREFERENCED_PARAMETER(lpReserved);
- switch (dwReason)
- {
- case DLL_PROCESS_ATTACH:
- case DLL_THREAD_ATTACH:
- g_Instance = hInstance;
- break;
- case DLL_THREAD_DETACH:
- case DLL_PROCESS_DETACH:
- break;
- }
- return TRUE;
- }
- LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam)
- {
- LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam;
- if(0 < nCode) {
- if(WM_RBUTTONDOWN == wParam) {
- if (NULL == g_GameWnd)
- {
- MessageBoxA(NULL, "Game window not found!", "Error", MB_OK);
- return CallNextHookEx( g_MouseHook, nCode, wParam, lParam );
- }
- int xPos = pMouseHook->pt.x;
- int yPos = pMouseHook->pt.y;
- CPoint point(xPos, yPos);
- CWindowDC dc(NULL);
- COLORREF cl = dc.GetPixel(point.x, point.y);
- ::ScreenToClient(g_GameWnd, &point);
- CString strMsg;
- strMsg.Format("x = %d, y = %d, color = %d", point.x, point.y, cl);
- MessageBoxA(NULL, strMsg, "Point Position & Color", MB_OK);
- }
- }
- LRESULT RetVal = CallNextHookEx( g_MouseHook, nCode, wParam, lParam );
- return RetVal;
- }
- BOOL InstallHook(HWND hWnd)
- {
- if (NULL == hWnd) {
- MessageBoxA(NULL, "Game window not found!", "Error", MB_OK);
- return FALSE;
- }
- g_GameWnd = hWnd;
- if(NULL != g_MouseHook) {
- MessageBoxA(NULL, "Hook already set!", "Error", MB_OK);
- return FALSE;
- }
- g_MouseHook = SetWindowsHookEx(WH_MOUSE, (HOOKPROC)MouseProc, g_Instance, 0);
- if(NULL == g_MouseHook) {
- MessageBoxA(NULL, "Hook can not be set!", "Error", MB_OK);
- return FALSE;
- }
- return TRUE;
- }
- BOOL UnInstallHook()
- {
- if(NULL == g_MouseHook) {
- MessageBoxA(NULL, "Hook not installed correct!", "Error", MB_OK);
- return FALSE;
- } else {
- if(UnhookWindowsHookEx(g_MouseHook))
- return TRUE;
- else {
- MessageBoxA(NULL, "Hook can not be removed!", "Error", MB_OK);
- return FALSE;
- }
- }
- }
另外在def文件中需要将InstallHook跟UnInstallHook导出。另外注意到在InstallHook中需要一个参数HWND,这个参数需要在调用这个DLL的客户端代码中获得。方法自然是用EnumWindows函数去枚举桌面上的所有窗口然后找到连连看窗口。
通过这个方式,然后加上一些简单的调式,或者如下四个数据:
- const int gc_CellStartX = 12; // 方块窗口起始点-X
- const int gc_CellStartY = 180; // 方块窗口起始点-Y
- const int gc_CellWidth = 31; // 每个方块的宽度
- const int gc_CellLength = 35; // 每个方块的长度
接下的需要做的也是最重要的就是在每个方格上取N个点来唯一的表示一种图案。那么N应该是多少呢?自然是越少越好,程序才能跑的快。之前我也是随机的在方块上取4-5个点,但是仔细一算一共209个小方块,那需要调用1000+次GetPixel,这大大影响了程序的运行速度。如果能用一个唯一的点的颜色会表示一个图案自然最好,但是这个似乎找起来有难度。不管三七二十一,我先取了中心点再说,看看究竟能区分多少方块。为了这个我再次使用到了钩子,这次的钩子我需要完成这件事情:在QQ连连看窗口上用鼠标右击某个图案,然后可以获得这个图案上特定点的位置,跟前一个钩子不同的主要是MouseProc函数:
- LRESULT WINAPI MouseProc(int nCode,WPARAM wParam,LPARAM lParam)
- {
- LPMOUSEHOOKSTRUCT pMouseHook=(MOUSEHOOKSTRUCT FAR *) lParam;
- if(0 < nCode) {
- if(WM_RBUTTONDOWN == wParam) {
- if (NULL == g_GameWnd)
- {
- MessageBoxA(NULL, "Game window not found!", "Error", MB_OK);
- return CallNextHookEx( g_MouseHook, nCode, wParam, lParam );
- }
- int xPos = pMouseHook->pt.x;
- int yPos = pMouseHook->pt.y;
- CPoint point(xPos, yPos);
- ::ScreenToClient(g_GameWnd, &point);
- int colIndex = (point.x - gc_CellStartX)/gc_CellWidth;
- int rowIndex = (point.y - gc_CellStartY)/gc_CellLength;
- ASSERT(rowIndex >= 0 && rowIndex < 11);
- ASSERT(colIndex >= 0 && colIndex < 19);
- int startx = gc_CellStartX + colIndex * gc_CellWidth;
- int starty = gc_CellStartY + rowIndex * gc_CellLength;
- CPoint pointCT(startx + gc_CellWidth / 2, starty + gc_CellLength / 2); // center point of the cell
- ::ClientToScreen(g_GameWnd, &pointCT);
- CPoint pointUP(pointCT.x, pointCT.y - 10);
- CWindowDC dc(NULL);
- COLORREF colorUP = dc.GetPixel(pointUP);
- COLORREF colorCT = dc.GetPixel(pointCT);
- CString strMsg;
- strMsg.Format("Row: %d, Column: %d/ncolorUP: %d/nncolorCT: %d/n", rowIndex, colIndex, colorUP, colorCT);
- MessageBoxA(NULL, strMsg, "Cell Color", MB_OK);
- }
- }
- LRESULT RetVal = CallNextHookEx( g_MouseHook, nCode, wParam, lParam );
- return RetVal;
- }
当然还要加上之前获得的4个常量才能通过编译。不难看出在这个MouseProc中我们使用了两个点唯一的区分一个图案,一个中心点一个中心点偏上10个像素。但是其实这并不是一开始就获得的结果,起初我就取了一个中心点,然后通过不断的测试,找到QQ连连看中每一种图案中心点的颜色值,结果发现中心点可以区分出44种不同图案中的39种,另外5种图案中共有两种颜色值。如下图所示,红颜色的两个图案中心点像素值相同,蓝颜色的三个图案中心点像素也相同。
有了这个结果基本可以断定两个点可以唯一的区分一个图案,我随意的取了中心点上方的10个像素的点作为第二个点,经过上述钩子程序的测试该点可以区分该5个点。为了尽量的加快程序速度,我并不是简单的每次选取所有图案的两个点,而是当必要的时候,也就是碰到中心点不能区分的时候再次去读取第二个点的像素值。为此,我使用了一个CellColor类唯一代表一个图案,提供了一点基本的操作:
- // class CellColor
- class CellColor{
- public:
- // Following static variables are used to make our application quicker...only for QQGame...
- static const COLORREF cBackGroundColor = 7359536;
- static const COLORREF cDuplicateColor1 = 16317688;
- static const COLORREF cDuplicateColor2 = 40184;
- public:
- CellColor(){ ;}
- CellColor(COLORREF color1, COLORREF color2)
- : m_color1(color1), m_color2(color2)
- {
- //
- }
- CellColor::CellColor(const CellColor &color)
- {
- m_color1 = color.m_color1;
- m_color2 = color.m_color2;
- }
- bool isBackGround()
- {
- return cBackGroundColor == m_color1;
- }
- bool equal(CellColor& color)
- {
- return m_color1 == color.m_color1 && m_color2 == color.m_color2;
- }
- private:
- COLORREF m_color1;
- COLORREF m_color2;
- };
下面的函数用来从QQ连连看窗口中获取一个图案的CellColor:
- //
- // Get each cell color. We choose two points for each
- // cell, because two points is enough to distinguish
- // all these patterns
- //
- CellColor GetEachCellColor(int startx, int starty)
- {
- // Two points is enough to distinguish all these patterns.
- CPoint point1(startx + gc_CellWidth / 2, starty + gc_CellLength / 2);
- CPoint point2(point1.x, point1.y - 10);
- COLORREF color1 = 0, color2 = 0;
- color1 =::GetPixel(g_GameDC, point1.x, point1.y);
- // Use following if statement to make it faster. Because GetPixel takes time.
- if(CellColor::cDuplicateColor1 == color1 || CellColor::cDuplicateColor2 == color1)
- color2 = ::GetPixel(g_GameDC, point2.x, point2.y);
- return CellColor(color1, color2);
- }
到目前为止,最需要的东西我们已经得到。已经足够写出一个最基本功能的连连看外挂。但是在完成最基本功能的同时我又突发奇想希望能完成这么一个功能:我的外挂可以根据其他对手的速率动态的调整自己的速度。有了这个想法,我需要获得更多的界面信息,当然主要是其他玩家分数(剩余方格数量),但是这可不是一个简单的活,虽然游戏窗口上明明白白写着一个100,但是我怎么才能获得这个数值呢?首先,我还是通过系统钩子的方法再加上严格的测试得到了每个玩家窗口中的包含分数的小矩形,通过截屏的方法我获得了玩家分数中从0到9的所有数值的特征点,放大后如下所示:
我所需要做的就是找到那么几个点唯一的确定这些数字,理论上可能4,5个就可以做到,然后一个人找啊找,结果发现这还真不是一件容易的活,反正vs在手,这还不是小菜一碟...
- #include <iostream>
- using namespace std;
- bool Numbers[11][8][5];
- void fill(int Num, int RowIndex, int ColIndex, int HorizontalNum = 1, int VerticalNum = 1)
- {
- if(Num > 10 || RowIndex + VerticalNum > 8 || ColIndex + HorizontalNum > 5) {
- cout<<"Wrong Number!"<<endl;
- return;
- }
- for(int i = 0; i < HorizontalNum; i++) {
- Numbers[Num][RowIndex][ColIndex + i] = true;
- }
- for(int i = 0; i < VerticalNum; i++) {
- Numbers[Num][RowIndex + i][ColIndex] = true;
- }
- }
- int main()
- {
- memset(Numbers, 0, 11 * 5 * 8);
- // 0
- fill(0, 0, 1, 3);
- fill(0, 1, 0, 1, 6);
- fill(0, 1, 4, 1, 6);
- fill(0, 7, 1, 3);
- // 1
- fill(1, 1, 1);
- fill(1, 0, 2, 1, 8);
- fill(1, 7, 1, 3);
- // 2
- fill(2, 0, 1, 3);
- fill(2, 1, 0, 1, 2);
- fill(2, 1, 4, 1, 2);
- fill(2, 3, 3);
- fill(2, 4, 2);
- fill(2, 5, 1);
- fill(2, 6, 0);
- fill(2, 7, 0, 5);
- // 3
- fill(3, 0, 1, 3);
- fill(3, 1, 0);
- fill(3, 1, 4, 1, 2);
- fill(3, 3, 2, 2);
- fill(3, 4, 4, 1, 3);
- fill(3, 6, 0);
- fill(3, 7, 1, 3);
- // 4
- fill(4, 0, 3, 1, 8);
- fill(4, 1, 2, 2);
- fill(4, 2, 1, 1, 2);
- fill(4, 4, 0);
- fill(4, 5, 1, 4);
- fill(4, 7, 3, 2);
- // 5
- fill(5, 0, 0, 5, 4);
- fill(5, 3, 0, 4);
- fill(5, 4, 4, 1, 3);
- fill(5, 6, 0);
- fill(5, 7, 1, 3);
- // 6
- fill(6, 0, 1,3);
- fill(6, 1, 0, 1, 6);
- fill(6, 1, 3);
- fill(6, 3, 0, 4);
- fill(6, 4, 4, 1, 3);
- fill(6, 7, 1, 3);
- // 7
- fill(7, 0, 0, 5, 2);
- fill(7, 0, 3, 1, 3);
- fill(7, 3, 2, 1, 5);
- // 8
- fill(8, 0, 1, 3);
- fill(8, 1, 0, 1, 2);
- fill(8, 1, 4, 1, 2);
- fill(8, 3, 1, 3);
- fill(8, 4, 0, 1, 3);
- fill(8, 4, 4, 1, 3);
- fill(8, 7, 1, 3);
- // 9
- fill(9, 0, 1, 3);
- fill(9, 1, 0, 1, 3);
- fill(9, 1, 4, 1, 6);
- fill(9, 4, 1, 4);
- fill(9, 6, 1);
- fill(9, 7, 1, 3);
- for(int m = 0; m < 11; m++){
- for(int i = 0; i < 8; i++) {
- for(int j = 0; j < 5; j++)
- {
- if(Numbers[m][i][j])
- cout<<"O";
- else
- cout<<" ";
- }
- cout<<endl;
- }
- cout<<endl<<endl;
- }
- for(int i1 = 0; i1 < 8; i1++)
- for(int j1 = 0; j1 < 5; j1++) {
- int i2 = (j1 == 4? i1 + 1 : i1);
- int j2 = (j1 == 4? 0 : j1 + 1);
- for(; i2 < 8; i2++)
- for(; j2 < 5; j2++) {
- int i3 = (j2 == 4? i2 + 1 : i2);
- int j3 = (j2 == 4? 0 : j2 + 1);
- for(; i3 < 8; i3++)
- for(; j3 < 5; j3++) {
- int i4 = (j3 == 4? i3 + 1 : i3);
- int j4 = (j3 == 4? 0 : j3 + 1);
- for(; i4 < 8; i4++)
- for(; j4 < 5; j4++) {
- int i5 = (j4 == 4? i4 + 1 : i4);
- int j5 = (j4 == 4? 0 : j4 + 1);
- for(; i5 < 8; i5++)
- for(; j5 < 5; j5++) {
- int i6 = (j5 == 4? i5 + 1 : i5);
- int j6 = (j5 == 4? 0 : j5 + 1);
- for(; i6 < 8; i6++)
- for(; j6 < 5; j6++) {
- int value[11] = {0};
- for(int index = 0; index < 11; index++) {
- int r1 = Numbers[index][i1][j1] << 5;
- int r2 = Numbers[index][i2][j2] << 4;
- int r3 = Numbers[index][i3][j3] << 3;
- int r4 = Numbers[index][i4][j4] << 2;
- int r5 = Numbers[index][i5][j5] << 1;
- int r6 = Numbers[index][i6][j6] << 0;
- value[index] = r1 + r2 + r3 + r4 + r5 + r6;
- }
- bool bSucess = true;
- for(int i = 0; i < 11; i++)
- for(int j = i + 1; j < 11; j++) {
- if(value[i] == value[j]) {
- bSucess = false;
- goto next;
- }
- }
- next: ;
- if(bSucess) {
- cout << "x1 = " << j1 << " y1 = " << i1 <<" x2 = "<<j2 <<" y2 = "<<i2 <<" x3 = "<<j3 <<" y3 = "
- << i3 << " x4 = "<< j4 << " y4 = "<< i4 <<" x5 = "<< j5<<" y5 = "<<i5<<" x6 = "<<j6<<" y6= "<<i6<<endl;
- for(int i = 0; i < 11; i++) {
- printf("%d: %d/n", i, value[i]);
- }
- return 0;
- }
- }
- }
- }
- }
- }
- }
- cout<<"Can not find six points to distinguish these numbers!"<<endl;
- return 0;
- }
写了程序才知道至少需要六个点才能区分这些个数字,怪不得我找了半年没找到,还好即使调转方向。除了这些信息之外,我还获得了开始按钮坐标等信息(实现自动开始,保障程序的连续运行)。OK,万事俱备了,来看看我们的收获吧:
- /
- // These const globe variables will help us to get all players' information
- // during a game. Of cause the main thing we care about is the score.
- // Here score means the number of left cells.
- const COLORREF gc_OtherPlayersBKGround = 11034624; // 是否有玩家
- const COLORREF gc_ScoreColor = 63728; // 分数颜色
- const int gc_FirstScoreRectStartX = 101; // 第一个玩家分数起始点 - X坐标
- const int gc_FirstScoreRectStartY = 44; // Y坐标,同上
- const int gc_SpaceBetweenTwoScoreRects = 119; // 两个玩家的分数间距
- const int gc_EachNumberRectWidth = 7; // 每个数字的宽度
- const int gc_EachNumberRectHeight = 10; // 每个数字的高度
- const int gc_MyScoreRectStartX = 505; // 自己的分数起始点 - X坐标
- const int gc_MyScoreRectStartY = 576; // Y坐标,同上
- /
- /
- // These globe variables are useful during the game. gc_StartFlagPoint
- // and gc_StartFlagColor are used to check wether the game has been started.
- // gc_StartButtonPoint is used to simulate the mouse click to start a game.
- // gc_SignificantPointsForSocre is used to get players' score.
- const CPoint gc_StartFlagPoint(311, 576); //
- const COLORREF gc_StartFlagColor = 10534136; // 结合上一个表示游戏是否已开始
- const CPoint gc_StartButtonPoint(670, 570); // 开始按钮的坐标点
- const CPoint gc_SignificantPointsForSocre[6] = {CPoint(0,1),CPoint(4,1),CPoint(4,2),CPoint(4,3),CPoint(0,4),CPoint(2,4)}; // 区分0-9的六个特征点
- /
到此为止第一步基本完成。
2. 连连看算法的实现
这个部分分为两部分,第一部分是遍历整个数组找到两个相同的图案,第二部分检查这两个图案是否连通。在介绍这部分工作之前需要简单介绍一下表示每个方块的数据结构及这些数据结构的填充:
- /
- // All player information during each game.
- struct Player{
- bool exist; // 是否有玩家
- int leftcells; // 玩家剩余方格数
- } g_AllPlayers[5] = {(0, 0), (0, 0), (0, 0), (0, 0), (0, 0)};
- /
- /
- // All cell information during each game.
- struct Cell{
- Cell(){ empty = true; }
- CellColor color; // 方块颜色,唯一确定一种图案
- CPoint postion; // 方块位置, 模拟鼠标点击
- bool empty; // 是否存在一个未消掉的方块
- } g_AllCells[gc_RowNum][gc_ColNum];
- /
- /
- UINT g_CellNum = 0; // My lefted cell number - used inside
- .....
- .....
- .....
- // Get all the cell information for the game. This must
- // be done after the game starts. We get all necessary
- // information here. The most important is to fill the
- // g_AllCells array.
- //
- void GetAllCellInfo()
- {
- // set new values for them
- for(int i = 0; i < gc_RowNum; i++)
- for(int j = 0; j < gc_ColNum; j++)
- {
- int x = gc_CellStartX + j * gc_CellWidth;
- int y = gc_CellStartY + i * gc_CellLength;
- g_AllCells[i][j].color = GetEachCellColor(x, y);
- g_AllCells[i][j].postion = CPoint(x + gc_CellWidth / 2, y + gc_CellLength / 2);
- }
- for(int i = 0; i < gc_RowNum; i++)
- for(int j = 0; j < gc_ColNum; j++) {
- if(!g_AllCells[i][j].color.isBackGround()) {
- g_AllCells[i][j].empty = false;
- g_CellNum++;
- }
- }
- }
不难看出,g_AllCells保存了所有方格信息,而g_CellNum保存了自己的剩余方格数。接下来就可以遍历g_AllCells这个数组找到图案相同的方块并检查是否连通。
第一部分代码如下:
- while(g_CellNum) {
- for(int i = 0; i < gc_RowNum; i++)
- for(int j = 0; j < gc_ColNum; j++) {
- if (!g_AllCells[i][j].empty) {
- for(int m = 0; m < gc_RowNum; m++)
- for(int n = 0; n < gc_ColNum; n++) {
- if(!g_AllCells[m][n].empty &&
- g_AllCells[i][j].color.equal(g_AllCells[m][n].color)) {
- if(!(i == m && j == n) && CanBeDeleted(i, j, m, n)) {
- SimulateMouseClick(g_AllCells[i][j].postion, g_AllCells[m][n].postion);
- g_AllCells[i][j].empty = true;
- g_AllCells[m][n].empty = true;
- g_CellNum -= 2;
- // 这里需要做很多事情...调整速度、检查输赢等...
- goto next;
- }
- }
- }
- next: ; // do nothing here, just start a new search.
- }
- }
- }
第二部分其实也就一个函数的实现: CanbeDeleted
- /
- // This function will check if two cells have any connect path
- // if so, true will be returned and then we simulate the mouse
- // click, else we return false and do nothing.
- //
- bool CanBeDeleted(int row1, int col1, int row2, int col2)
- {
- // First we check this kind of path: two x-lines and one y-line.
- // one x-line and one y-line or only one x-line are all special
- // cases, will also be handled by following code.
- int left1 = col1, right1 = col1;
- int left2 = col2, right2 = col2;
- if(col1 != 0)
- while(left1 > 0 && g_AllCells[row1][left1 - 1].empty){ left1-- ;}
- if(col1 != gc_ColNum - 1)
- while(right1 < gc_ColNum - 1 && g_AllCells[row1][right1 + 1].empty){ right1++ ;}
- if(col2 != 0)
- while(left2 > 0 && g_AllCells[row2][left2 - 1].empty){ left2-- ;}
- if(col2 != gc_ColNum - 1)
- while(right2 < gc_ColNum - 1 && g_AllCells[row2][right2 + 1].empty){ right2++ ;}
- ASSERT(left1 >= 0 && left2 >=0 && right1 < gc_ColNum && right2 < gc_ColNum);
- int commonLeft = max(left1, left2);
- int commonRight = min(right1, right2);
- if(commonLeft <= commonRight) {
- int upCellRowIndex = min(row1, row2);
- int downCellRowIndex = max(row1, row2);
- for(int i = commonLeft; i <= commonRight; i++) {
- int upCellRowIndexTmp = upCellRowIndex;
- int downCellRowIndexTmp = downCellRowIndex;
- if(i == col1) {
- if(row1 == upCellRowIndexTmp)
- upCellRowIndexTmp++;
- else
- downCellRowIndexTmp--;
- }
- if(i == col2) {
- if(row2 == upCellRowIndexTmp)
- upCellRowIndexTmp++;
- else
- downCellRowIndexTmp--;
- }
- if(downCellRowIndexTmp < upCellRowIndexTmp)
- return true;
- while(g_AllCells[upCellRowIndexTmp][i].empty) {
- if(upCellRowIndexTmp++ == downCellRowIndexTmp) {
- // We do find a path here, return true.
- return true;
- }
- }
- }
- }
- // First we check this kind of path: two y-lines and one x-line.
- // one x-line and one y-line or only one y-line are all special
- // cases, will also be handled by following code.
- int up1 = row1, down1 = row1;
- int up2 = row2, down2 = row2;
- if(row1 != 0)
- while(up1 > 0 && g_AllCells[up1 - 1][col1].empty){ up1-- ;}
- if(row1 != gc_RowNum - 1)
- while(down1 < gc_RowNum - 1 && g_AllCells[down1 + 1][col1].empty){ down1++ ;}
- if(row2 != 0)
- while(up2 > 0 && g_AllCells[up2 - 1][col2].empty){ up2-- ;}
- if(row2 != gc_RowNum - 1)
- while(down2 < gc_RowNum - 1 && g_AllCells[down2 + 1][col2].empty){ down2++ ;}
- ASSERT(up1 >= 0 && up2 >=0 && down1 < gc_RowNum && down2 < gc_RowNum);
- int commonUp = max(up1, up2);
- int commonDown = min(down1, down2);
- if(commonUp <= commonDown) {
- int leftCellColIndex = min(col1, col2);
- int rightCellColIndex = max(col1, col2);
- for(int i = commonUp; i <= commonDown; i++) {
- int leftCellColIndexTmp = leftCellColIndex;
- int rightCellColIndexTmp = rightCellColIndex;
- if(i == row1) {
- if(col1 == leftCellColIndexTmp)
- leftCellColIndexTmp++;
- else
- rightCellColIndexTmp--;
- }
- if(i == row2) {
- if(col2 == leftCellColIndexTmp)
- leftCellColIndexTmp++;
- else
- rightCellColIndexTmp--;
- }
- if(rightCellColIndexTmp < leftCellColIndexTmp)
- return true;
- while(g_AllCells[i][leftCellColIndexTmp].empty) {
- // We do find a path here, return true.
- if(leftCellColIndexTmp++ == rightCellColIndex) {
- return true;
- }
- }
- }
- }
- // No connect path find if we get here. Return false.
- return false;
- }
这个函数包含了两部分:检查X-Y-X连通及Y-X-Y连通。当然包括X-Y等其他特殊情况。两部分代码基本完全一致,不难看懂。
到目前为止,大部分工作已经完成了。接下来我们需要模拟鼠标的点击事件。
3. 鼠标事件的模拟
整个程序有两个地方需要模拟鼠标点击,第一当然是去消掉那些方块,第二我们用于模拟点击开始按钮,这样我们就可以保证程序连续运行,实现真正的挂机。分别采用了如下的方法:
- /
- // Simulate the mouse click every time we find two cells
- // that can be deleted.
- //
- void SimulateMouseClick(CPoint pt1, CPoint pt2)
- {
- ::SendMessage(g_GameWnd, WM_LBUTTONDOWN, 0, (LPARAM)MAKELONG(pt1.x, pt1.y));
- ::SendMessage(g_GameWnd, WM_LBUTTONUP, 0, (LPARAM)MAKELONG(pt1.x, pt1.y));
- ::SendMessage(g_GameWnd, WM_LBUTTONDOWN, 0, (LPARAM)MAKELONG(pt2.x, pt2.y));
- ::SendMessage(g_GameWnd, WM_LBUTTONUP, 0, (LPARAM)MAKELONG(pt2.x, pt2.y));
- }
- ......
- ......
- ......
- // 模拟鼠标点击开始按钮的代码
- // Click the start button, waiting for other players to start.
- if(gc_StartFlagColor != ::GetPixel(g_GameDC, gc_StartFlagPoint.x, gc_StartFlagPoint.y)) {
- // Simulate to click the start button on the game window.
- CPoint pt(gc_StartButtonPoint);
- ::ClientToScreen(g_GameWnd, &pt);
- SetCursorPos(pt.x, pt.y);
- mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0);
- mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0);
- // Keep waiting if the game window remains unchanged.
- while(::GetPixel(g_GameDC, gc_StartFlagPoint.x, gc_StartFlagPoint.y) != gc_StartFlagColor){;}
- }
好了,到此为止程序已经可以跑起来了...并且可以实现秒杀了...但是在我的外挂中另外还实现了其他一些简单的功能。从下面的截图中我们可以看出来程序的其他一些功能。
我们可以看到程序可以有手动和自动两种方式运行。我们可以在程序运行的时候动态地拖动滚动条来更改程序运行速度,当然我们需要采用多线程编写。手动调整速度的实现比较简单,我们只需根据滚动条的位置计算出一个较为合理的等待时间,然后调用sleep函数就可以了。自动方式较为复杂,我需要根据其他玩家的速度动态调整我的速度,虽然功能是实现了,基本可以保证赢得看不出来,但是还是有很多地方需要改进。
另外需要说明的是,虽然这个外挂是基于无道具版本写的,但是具有一定的处理道具的能力。我的实现方式如下:每次找到两个可以消掉的方块后先进行鼠标模拟点击,然后从界面上获得自己的剩余方格数量与程序内部的方格数量g_CellNum进行比较,如果发现两个不符则说明遇到障碍、禁手、或者镜子等道具,等待一定时间后重新获取界面各种信息继续运行,这样在一定程度上保证了程序的持续运行性。但是改版本暂时不能处理重列功能,当遇到无解的时候需要手动重列。虽然要实现这个功能不难,但是考虑到该版本是使用了模拟鼠标的方法实现的,很难做到完美,就在下一个版本中增加这个功能了。
另外再开发的时候还遇到了一个难点,就是如何判断输赢。每次消掉两个后程序会等待一个时间然后再继续消掉下两个,但是在这段时间中对手可能已经赢了,游戏结束,但是我的程序确还在"消掉"。这显然不是我们期望的,因为在整个程序中,我没有使用sleep函数去等待。而是在每次等待的时间内不断的去查看其它玩家的剩余方块数,如果检测到零则说明游戏结束,其他玩家已经胜利。如下函数帮我实现了这个目的:
- /
- // This function is used to wait for a provided wait time and
- // check the game state. If anyother player has finished this
- // game we return flase.
- //
- bool WaitAndCheckGameState(int waitingTime)
- {
- DWORD startTime = ::GetTickCount();
- do {
- // 这里的GetMostCompetitiveScore用来获得最快玩家的剩余方块数
- if(0 == GetMostCompetitiveScore())
- return false;
- }
- while(::GetTickCount() <= startTime + waitingTime);
- return true;
- }
通过这种方式,我可以基本地判断出游戏的胜负。但是还有一种情况暂时无法处理,就是当遇到其他玩家也使用外挂并且都是用秒杀的时候,这时胜负很难判断。程序会经常判断错误。在以后的版本中会进行改进。
遇到的问题:
1. 在HOOK中使用MessageBox似乎有问题,当用鼠标右击的时候有偶然性。不是每次都能弹出。网上查了一下好像也说在HOOK中不易使用MessageBox。具体原因有待研究...
2. 在多线程中使用InstallHook好像不能正常使用HOOK,没找到什么原因,因为在这个外挂中使用到了多线程来保证界面的可交互性,当我在开始按钮点击的相应函数中调用InstallHook可以正常使用,但是在AfxBeginThread函数启动的新线程中调用InstallHook好像不能正常使用HOOK。
3. 开始我的程序全部使用了CWindowDC作为获取像素值的DC,后来改为用::GetPixel(g_GameDC,...),但是发现用这两种方法获取的同一个点的像素值不同,不知道是不是正常现象。
4. 在模拟鼠标点击开始按钮的时候我开始也使用了::SendMessage()函数去给游戏窗口发送消息,但是没有成功。后来只能选择用mouse_event来实现这个功能。但是在这个程序中mouse_event函数比起::SendMessage()函数有不足之处。我们需要保持窗口处于最顶层然后进行点击,造成了很多不便。不知道是何原因,可能还需要深入了解外挂知识。
希望在今后的学习中能找到这些问题的原因及解决方法。
程序存在的问题:
目前来说程序还存在很多问题:
1. 没有强大的道具处理能力。在出现道具的情况下程序有时会失灵(我的错...有bug没解掉T_T)。
2. 在运行时需要将游戏窗口置于在前端,要不然也会有问题。因为要获取颜色、用mouse_event等都需要将游戏窗口置前。
3. 在遇到更强大的外挂的时候秒杀拼不过人家...郁闷...这个估计跟外挂的类型有关了...等待下一个版本的完成(通过伪造数据包的方法应该会更快更强大)。
4. 本来以为能解决前面三个问题程序就比较完美了,没想到在写这篇文章的时候花生米找我玩连连看,牛人啊,开口就让我调半秒。一玩果然不亏zju连连看老大啊,速度相当快。他提出了很多建设性意见,可以更好的伪装。原来大牛们看外挂有自己的一套啊。赶紧记下来,在以后的版本中改进改进:1.高手都用重列和指南针。本来以为用指南针会影响高手速度,原来不是的,高手都是重列完马上指南针然后可以连击下去。见识了见识了。2. 很多外挂不符合人眼规则,就是不消近的消远的。一直以为高手外连连看都狂点狂点,想不到还有心思研究这个。牛啊。另外他还跟我说了一个秘密哈哈,全中国的连连看高手都在无道具场3和10。果然是高手,唉,真的见识了。下次再写一个厉害一点的让他去鉴定哈哈。
5. 今天在测试的时候又遇到了一个新的问题...在vista下不能用...唉...问题多多,期待下一个版本的完成吧...
在整个程序中也遇到了一些问题,至今还未解决,暂且一一列出: