欢迎来到 嗅灵易学

零基础也能上手的脚本技术课,一对一答疑带你入门

[原创]新人入手第一个游戏外挂,附上详细制作过程

[原创]新人入手第一个游戏外挂,附上详细制作过程

此教程为原创,但本人是个菜鸟,刚入手写程序,因此本帖有部分内容是参照其他一些教程,以及本论坛里有关连连看的帖子,再经过一些修改,再附上制作过程,以及本人的一些经验.
经过这次的练习,才知道很多东西要通过自己动手去做,才能领悟```其中有不对之处,希望有高人能够指点一二,希望此贴能够对像我一样的纯菜鸟有帮助.(发帖子,不懂技巧,附件都被吞了,可以下载二楼的[复件 QQ连连看游戏外挂详解教程.rar]文件.
本教程分为几大部分
一、  先创建外挂对话框,定义外挂所需要的基本功能
二、  找出棋盘数组的地址,读出棋盘数据
三、  编写实现消去棋子的代码框架
四、  实现消去棋子功能
五、  修补与完善外挂

一、  先创建外挂对话框,定义外挂所需要的基本功能
1.   创建对话框:   创建一个MFC工程对话框
打开VC++软件  选择??------>(File)文件------>New(新建)
如图:

 
选择--->Project(工程文件)---->MFC AppWizard[exe],填写好工程名字,选择好文件路径,点击OK 
2.  选择---->Dialog based(基本对话框)------->Finish(完成)------>OK,如下图:

3.  先删除了原来的按钮(选中按delete),再添加我们所按钮.(点一下想要添加的按钮或控件,再到对话框内点击一下,即可添加)如图:

 
4.  再把按钮的属性先改好(ID与标题)
在所需要改的按钮上右键---->Properties(属性),

 
图上添加了两个按钮,两个复选框,跟一个滑块.
一些基本功能都差不多了,后面如果还需要,再按以上方法添加.
二、  找出棋盘数组的地址,读出棋盘数据
1.  找出棋盘数组地址,我们需要用到一个Cheat Engine(CE)软件,(这些工具大多都是英文的,对于外语我可是一点也不懂,所以,工具的单词我都去网上翻译一下,如果有不对的地方,请理解,明白意思就行了)。

 
2.  这CE工具主要是访问进程地址空间,然后通过枚举的方法找出想要找的地址。
在枚举地址之前,还要用到另外一个工具XueTr.exe,因为QQ游戏大厅如果检测到CE工具在运行的话,就会弹出一个警告的对话框,然后退出游戏。

 
用XueTr.exe工具能解决弹出警告,步骤:
打开QQ游戏大厅(要先关掉CE,免得又弹出警告),再打开XueTr.exe--->点击 进程 ---->右击QQ游戏大厅进程----->选择  查看进程模块;

 
找到 Tersafe.dll,右键点击  Tersafe.dll ----->删除模块文件.
然后再返回进程右键点击 QQ游戏大厅----->选择 查看进程线程---->点击一下 模块 让他排序好---->右键点击 TenSLX.dll----->选择 结束线程(不管有几个TenSLX.dll,都结束掉).如下图:

好了,现在可以关掉XueTr.exe了,打开CE工具就不会再弹出警告了,当然QQ游戏大厅每次重新启动都需要用XueTr.exe来结束那几个线程,才能正常使用CE.(那个Tersafe.dll模块已经删掉了,就没有了,也就不用再次删除了)
3.

如上图,我们知道棋盘是一个11*19的二维数组的结构,我们只要找到数组的基地址就能算出整个数组每个棋子的位置,就是说只要枚举出上图做记号的那个位置的地址就行了.每一个棋子都是用一个数值来表示的,不同的棋子,用不同的数表示,空的地方当然就是0了,假如说1代表一种棋子,2又代表另一种棋子,依此类推,那么整个棋盘的最大范围都不会超过255(一个字节范围)所以我们枚举棋盘地址的时候选择数据类型为字节就足够了,如图:

 
4.  现在进入QQ连连看游戏房间,找位置坐下,点击练习,再打开CE工具---->点击 选择进程打开----->选择 连连看游戏进程---->Open(打开)
如图:


 
如上图,我们要找的地址没有棋子,就是为0,那么用CE工具就选择Exact Value(精确值)再点New Scan(新扫描)…如下图:

 
我们看到上图左边Found(扫描结果)处:有一个很大的数值,再下来的Address(偏移地址)与Value(值)的地方是表示符合条件的结果;(数值都为0)
然后我们再点游戏的 练习,换一盘棋,

 
请看左上角的地方还是没有棋子,就再选择Exact Value(精确值)再点Next Scan(再次扫描)…如下图:

 
我们会发现扫描结果已经在减少;
再次点击 练习,更换棋子.

上图左上角已经有一个棋子了,那么CE就要选择Bigger than(大于)------->Next Scan(再次扫描)

现在发现扫描结果明显少了很多,然后再点击练习,换棋子,如果左上角没有棋子,就选择Exact Value(精确值)为0, ------->Next Scan(再次扫描); 如果有棋子就选择Bigger than(大于)------->Next Scan(再次扫描).直到扫描结果剩下一个的时候,就是棋盘数组的基地址了(在扫描到后面剩下几个扫描结果的时候,可能遇到再怎么换棋子来扫描,结果都不会减少的情况,这时可以退出游戏,然后换个座位或者换个房间,CE工具再重新选择进程打开,接着扫描)

现在找到了棋盘地址,我们验证一下这地址正不正确,双击该地址,该地址就会在如上图出现在CE工具的下面,右键该地址----->Browse this memory region(浏览此内存区域) 

此时,就能看到棋盘地址0x0012A1F8处的内容,再去跟棋盘的棋子对应,就会发现,都是可以吻合的,(当然,如果不吻合,肯定是地址不正确了,)如图:

看上图,第一排是空的,第二排的第一格也为空,加起来就一共是20个空的,再看下图,0x0012A1F8处,前面一开始也是有20字节为0的,紧接着两个字节为0C跟08,再看棋盘也刚好有两个棋子.(当你在棋盘上消去一对棋子的时候,会看到地址表中对应的两个字节,也会跟着被设为零)
现在基本上可以肯定这棋盘地址是正确的了:0x0012A1F8  //棋盘基地址

5.  在读出棋盘数据之前,我们还需要知道窗口的标题,可以用VC++的Spy++工具,打开VC++选择Tools(工具)------>Spy++,然后点击“小望远镜”的图标.如下图:

在弹出来的对话框中选定Properties(属性),打开连连看游戏,用鼠标按住Finder Tool(查找工具)的图标,把它拖动到连连看的游戏窗口中


 
然后点击OK,

如上图中,在General(常规)中的Window Caption(窗口标题中)就可以直接把窗口标题复制下来.
首先我们回到VC++,在对话框中多添加一个按钮与编辑框,用来读出棋盘数据.

用来读出棋盘数据的编辑框,大小要调整合适,然后再修改编辑框的属性
选定该编辑框----->右键------> Properties(属性)--->Styles(样式),然后把Multiline(多行)与Want return(这里的意思应该是换行)前面钩上,如下图:

(这个属性更改好了,直接关掉属性对话框,或者按回车,就会回到编辑外挂对话框了)
然后再给编辑框关联一个类变量,右键编辑框----->Class Wizard(类向导)---->Member Variables(成员变量)------>选定编辑框ID(我没有更改该编辑框的ID,所以它是IDC_EDIT1)----->Add Variables(添加变量).如下图:

在弹出的对话框中设置好Member variable name(变量类型)跟Variable type(变量名,因为棋盘数据是一个字符串数组,所以我们要选择字符串类型)---->OK.
如下图:
 
返回外挂编辑图,双击”读取棋盘数据”按钮就可以转到实现触发该按钮消息的成员函数.我们在这里编写代码实现读出棋盘数据.如下图:
 
先为棋盘地址与窗口标题定义一个宏(定义在函数外面,要全局的,)
#define CHESS_DATA_ADDRESS 0x0012A1F8
#define GAME_CAPTION “QQ游戏 - 连连看角色版”
byte chessdata[11][19];  //存放棋盘数据的数组,这个数组我们定义为全局变量,因为别的函数还要用到此数组.
void CQQllkWgDlg::OnButton1() 
{
  // TODO: Add your control notification handler code here
  
  DWORD prcesssid;      //返回窗口进程ID
  LPVOID chessaddress = (LPVOID)CHESS_DATA_ADDRESS;
  DWORD byread;
  
        HWND hwnd = ::FindWindow(NULL,GEAM_CAPTION);  //获取窗口句                       柄
        if (hwnd == NULL)//如果打开窗口不成功
        {
            GetWindowThreadProcessId(hwnd,&prcesssid); //获取窗口进程ID
            HANDLE processsH = OpenProcess(PROCESS_ALL_ACCESS,FALSE, prcesssid) ;//打开指定进程
            ReadProcessMemory(processsH,chessaddress,chessdata,19*11,&byread); //读取进程指定地址、大小的数据(读出数据到chessaddress数组)
            CloseHandle(processsH);//关闭进程     
         }
}
//(编译代码,看是否有错误,我们往后每完成一小部分代码都应该编译一下,看是否有错,有错误就要先改过来,再继续写下去)

现在已经读出棋盘数据,那么我们再把chessaddress数组的数据显示到之前定义变量为m_ChessData的编辑框,这样我们能更清楚地验证棋盘数据.
    
      CString chessdata1;
                CString tempStr;
     for(int x = 0;x<11;x++)
     {
       for (int y = 0;y<19;y++)
       {
         tempStr.Format("%2x",chessdata[x][y]);//把数据进行排版
         chessdata1 += tempStr;
         }
       chessdata1 += "\r\n";
     }
     m_ChessData = chessdata1;
           UpdateData(FALSE);//更新数据到编辑框

把上面代码加入到void CQQllkWgDlg::OnButton1()函数读取棋盘数据的代码后面,编译通过后,运行程序,就能显示棋盘数据到编辑框.如下图:
 
 
现在已经能显示棋盘数据了,不过看起来,不是很好看,再修改一下:
把tempStr.Format("%2x",chessdata[x][y]); 里面的%2x改成%2.0x;在
chessdata1 += tempStr;后面再上chessdata1 += ” ”;这样看起来会更加好看.
 
 
这样看起来更加贴切```
既然读出了棋盘数据,那么接下来就是准备实现消去一对棋子代码框架.
在写代码前,我们先建立一个头文件,然后我们就在头文件中编写实现的代码,这样会比较整洁,清晰
点击菜单File(文件)--->New(新建)--->Files--->C/C++ Header File,写上头文件名,如下图:
 
在VC++应用于软件的左边,FileView版块的Header Files文件夹中我们就能看到刚才新建的头文件了,后面我们的一些函数的实现,都写在这头文件中,如下图:

三、  编写实现消去棋子的代码框架
1.  我们得到了棋盘数据,每个棋子都比较一次,如果相同且不为0,就调用检测函数(这些函数,我们自己写代码实现)检测是否能消去,如果能消去,再调用实现消去棋子函数.
以下代码实现遍历整个棋盘.(这函数我们写到GameProcess.h头文件中去)
BOOL ClearPiar()
{
  UpdateChess();//更新棋盘数据
  //遍历整个棋盘,找出相同的一对棋子
  
  POINT p1 = {0};//定义两个POINT型结构的变量来表示两棋子的位置.
  POINT p2 = {0};
  int x1 = 0;
  int x2 = 0;
  int y1 = 0;
  int y2 = 0;
  for (y1 = 0;y1 < 11;y1++)
  {
    for (x1 = 0;x1 < 19;x1++)
    {
      for (y2 = y1;y2 < 11;y2++)//遍历的时候要注意y2 = y1
      {
        for (x2 = 0;x2 < 19;x2++)//x2 = 0
        {
          if (chessdata[y1][x1] == chessdata[y2][x2])//如果棋子相等
          {
            if (chessdata[y1][x1] != NULL)//如果棋子不为零
            {
              if ((x1 != x2)||(y1 != y2))//两棋子不是同一棋子
              {
                p1.x = x1;
                p1.y = y1;
                p2.x = x2;
                p2.y = y2;
                if (checkchess(p1,p2))//检测棋子是否能消除
                {
                  click2p(p1,p2);//消除一对棋子
                  
                  return TRUE;
                }
              }
            }
          }
        }
      }
    }
  }
  return FALSE;
}

我们给上面用到的几个函数先定义个空函数,
BOOL checkchess(p1,p2)
{
  return TRUE;
}
BOOL click2p(p1,p2)
{
  return TRUE; 
}
Void UpdateChess()
{
}

2.  这些函数必须定义在ClearPiar()之前,(因为我们在定义ClearPiar()之前没有给这些声明过,)要不编译会出现未定义错误.
现在我们只要完成checkchess(p1,p2)函数跟click2p(p1,p2)函数还有UpdateChess()函数(这更新棋盘数据的函数,其实我们已经完成了,之前我们写过显示棋盘数据的成员函数,只要去掉后面显示棋盘数据部分就行了)的实现,就差不多能实现消除一对棋子了.
这些函数我们都写到GameProcess.h头文件里面,打开该头文件,把QQllkWgDlg.cpp中的
#include "stdafx.h"复制过去.
#define CHESS_DATA_ADDRESS 0x0012A1F8
#define GAME_CAPTION “QQ游戏 - 连连看角色版”
byte chessdata[11][19];  //存放棋盘数据的数组,这个数组我们定义为全局变量,因为别的函数还要用到此数组.
Void UpdateChess()
{
    DWORD prcesssid;      //返回窗口进程ID
    LPVOID chessaddress = (LPVOID)CHESS_DATA_ADDRESS;
    DWORD byread;
  
    HWND hwnd = ::FindWindow(NULL,GEAM_CAPTION);  //获取窗口句                       柄
    if (hwnd != NULL)//如果打开窗口不成功
    {
       GetWindowThreadProcessId(hwnd,&prcesssid); //获取窗口进程ID
       HANDLE processsH = OpenProcess(PROCESS_ALL_ACCESS,FALSE, prcesssid) ;//打开指定进程
       ReadProcessMemory(processsH,chessaddress,chessdata,19*11,&byread);//读取进程指定地址、大小的数据(读出数据到chessaddress数组)
       CloseHandle(processsH);//关闭进程
     }
}

3.  我们已完成UpdateChess()函数了,之前创建的”读出棋盘数据”的按钮跟用来显示棋盘数据的编辑框,已经没有什么用了,先把它们给删除掉.首先点击一下实现读出棋盘数据的void CQQllkWgDlg::OnButton1()函数,再点击右上角的”GO”图标:
 
然后把这函数的声明给注释掉,如下图:
 
再双击左侧QQllkWgDlg.cpp回到函数的实现,把该函数的代码全都注释掉(包括全局变量与宏定义).点击左侧的ResourceViewt版块--->QQlldWg resources--->Dialog,双击IDD_QQLLKWG_DIALOG,返回到对话编辑,右键点击”显示棋盘数据的编辑框”,如下图:
  
---> Class Wizard(类向导)---->Member Variables(成员变量)------>选定之前定义的m_ChessData变量--->Delete Variable(删除变量)--->OK.
 
再同时选定”读出棋盘数据”按钮跟显示棋盘数据的对话框,按Delete键删除掉.
再编译看看,有错误,不能通过,双击出错的提示,去到出错的地方,我们看到还有个地方跟”读出棋盘数据”按钮有关系的,把它也注释掉.再编译就能通过了,如下图:
 
4.  在写checkchess(p1,p2)函数之前,先来分析一下连连看游戏规则,玩过连连看游戏的人,都应该知道,要消除两个棋子的条件是最多只能用三条直线(路线)连起
来.如下图:
 
 
从上图中,我们知道最多只能三条线相连的话,不管怎么变化,无非就那么几种情况(还有两个棋子连在一起的情况),只要我们一一检测一遍,就知道能否消除了,
首先,我们写一个检测一条直线是否能连通的函数,这个比较容易实现(因为一条线能连起来的话,不是x轴相同,就是y轴相同).
 
BOOL Linkchess(POINT p1,POINT p2)
{
  POINT temp = {0};
  int i = 0;
  if (p1.y == p2.y) //如果两棋子同在x轴上
  {
    if (p1.x > p2.x)//始终设左边的棋子为p1,右边的棋子为p2
    {
      temp = p1;
      p1 = p2;
      p2 = temp;
    }
    for (i = p1.x+1;i < p2.x;i++) //p1.x+1为从左边棋子往右的一个棋子开始检测
    {
      if (chessdata[p1.y][i] != NULL)//如果线路中有一个格子不为0,那么此线路不通
      {
        return FALSE;
      }
    }
  }
  if (p1.x == p2.x) //如果两棋子同在y轴上
  {
    if (p1.y > p2.y)//始终设上面的棋子为p1,下面的棋子为p2
    {
      temp = p1;
      p1 = p2;
      p2 = temp;
      
    }
    for (i = p1.y+1;i < p2.y;i++) //p1.y+1为从最顶的棋子往下面的一个棋子开始检测
    {
      if (chessdata[i][p1.x] != NULL)//如果线路中有一个格子不为0,那么此线路不通
      {
        return FALSE;
      }
      
    }
  }
  return TRUE;  //整条线中都为0,表示能通
}

如果两棋子是相邻的情况, Linkchess(p1,p2)函数中的for循环是不能够满足条件的,所以也会返回TRUE.
上面的Linkchess(p1,p2)函数检测了一条线路是否能连通,三条线路的情况,就调用三次Linkchess(p1,p2)函数,只要每次都返回TRUE的话,也就说明能连通;
BOOL checkchess(POINT p1,POINT p2)
{
  POINT A = {0};
  POINT B = {0};
  int i = 0;
  if (p1.x == p2.x || p1.y == p2.y) //如果y轴或者x轴相同,(第一种情况)
  {
    if (Linkchess(p1,p2))         //检测一条线路
    {
      return TRUE;
    }
  }
  if ((chessdata[p1.y][p2.x] == NULL) || (chessdata[p2.y][p1.x] == NULL))//如果x轴与y轴相交的两个点有一个为0(第二种情况)
  {
    A.x = p1.x;
    A.y = p2.y;  //交点赋值于临时变量A
    B.x = p2.x;
    B.y = p1.y;  //另一个交点赋值于临时变量B
    if ((Linkchess(p1,A) && Linkchess(p2,A) && (chessdata[p2.y][p1.x] == NULL)) //检测两条线路,交点必须为0
      || (Linkchess(p1,B) && Linkchess(p2,B) && (chessdata[p1.y][p2.x] == NULL)))
    {
      return TRUE;
    }
  }
  if (p1.x != p2.x)  //两棋子不在同一y轴上(第三种情况纵向检测)
  {
    A = p1;
    B = p2;
    
    for (i = 0;i < 11;i++)
    {    
      A.y = B.y = i;
      if ((chessdata[A.y][A.x] == NULL) && (chessdata[B.y][B.x] == NULL) 
        && Linkchess(p1,A) && Linkchess(p2,B) && Linkchess(A,B)) //纵向循环检测三条线路,两交点也必须为0
      {
        return TRUE;
      }
    }
  }
  if (p1.y != p2.y) //两棋子不在同一x轴上(第三种情况横向检测)
  {
    A = p1;
    B = p2;
    
    for (i = 0;i < 19;i++)
    {    
      A.x = B.x = i;
      if ((chessdata[A.y][A.x] == NULL) && (chessdata[B.y][B.x] == NULL) 
        && Linkchess(p1,A) && Linkchess(p2,B) && Linkchess(A,B)) //横向循环检测三条线路,两交点必须为0
      {
        return TRUE;
      }
    }
  }
  return FALSE;
}

  
在checkchess(p1,p2)函数中,每一种路线的情况用一个if语句表示(不算嵌套的),
i.  对于用一条线路就能连接的情况,两棋子肯定是在同一直线上的, 
if (p1.x == p2.x || p1.y == p2.y)只要y轴相同或者x轴相同,就调用Linkchess(p1,p2)检测路线,能连通返回TRUE.
ii.  用两条线路的情况,就是p1与p2的x轴跟y轴的两个交点,这两个交点是固定且是可以算出来的,所以检测这两组形成交点的直线是否能连通,以及交点本身是否为0;
if ((chessdata[p1.y][p2.x] == NULL) || (chessdata[p2.y][p1.x] == NULL)),只要有一个为0,再检测线路是否能连通.
if ((Linkchess(p1,A) && Linkchess(p2,A) && (chessdata[p2.y][p1.x] == NULL)) 
      || (Linkchess(p1,B) && Linkchess(p2,B) && (chessdata[p1.y][p2.x] == NULL)))两组路线,只要有一组能连通以及两线的交点为0.返回TRUE.
iii.  三条线路的话,就有两个交点,还分横向,跟纵向,而且三条线路中,有一条是不确定的,所以用个for循环来检测,按照棋盘数组,纵向循环11次,横向循环19次,检测两个交点是否为0,以及三条线路是否能通,满足这5个条件返回TRUE.
  for (i = 0;i < 11;i++)
    {    
      A.y = B.y = i;
      if ((chessdata[A.y][A.x] == NULL) && (chessdata[B.y][B.x] == NULL) && Linkchess(p1,A) && Linkchess(p2,B) && Linkchess(A,B)).
5.  能消除的一对棋子已经找出来了,那么我们下面开始实现模拟鼠标点击,消去一对棋子,现在我们要找出棋盘数组相对于窗口的坐标:
这个我们也用Spy++,也很容易能够实现: 这次我们用Spy++查找窗口消息,选定Message(消息),用鼠标按住Finder Tool(查找工具)的图标,把它拖动到连连看的游戏窗口中,点OK.如下图:
 
此时,我们会看到不断地截取到很多消息.如下图:
 
然后点击暂停图标,停止获取消息,再点击删除图标,删除掉这些消息,最后点击选项图标,在弹出来的对话框的Messages版块中,点击Clear All(删除全部)删除所有的消息,如下图:
 
然后选中鼠标左键按下与鼠标左键抬起的两个消息,点击OK.如下图:
 
然后再点击一下开始截取消息图标:如下图
 
现在把鼠标光标移动到游戏棋盘的第一格(左上角),点击一下鼠标左键,如下图:
 
现在可以看到了两个消息,(一个按下,一个抬起).如下图:
 
图中两个消息表示在相对窗口坐标的x = 23,y = 194的位置按下跟抬起了一次鼠标左键,就是说明棋盘第一个棋子相对于窗口的位置是x = 23,y = 194.再双击一下两个消息中的一个,就可以看到LParam参数,如下图:
 
LParam = 0x00C20017是一个32位的值,其实这个值的高16位0x00C2就是十进制的194,那低16位0x0017对应的十进制就是23了;现在只要再知道棋子的高跟宽就能算出棋盘数组中每一个棋子的坐标了,求棋子的宽度与高度,我们用最直接的方法,就是截取一张整个棋盘的图片(大小跟棋盘偏差不要太大),然后用一个图片编辑工具打开(我用的是Windows自带的图片编辑器),就能看到该图片的宽度与高度,然后宽/11,高/19,用四舍五入取整数的商,就是一个棋子的宽与高了,看下图:
 
上图中,棋盘的宽为594,高为388.
那么594/19≈31     388/11≈35
参数LParam就等于(y<<16) + x.
那么,如果棋子p1的坐标是x = 2,y = 3的话,LParam就表示为(35*3<<16) + 31*2;还要注意的是这个坐标是相对于棋盘第一格的坐标的,要表示相对窗口的坐标的话,还要加上第一格的坐标,LParam = ((35*p1.y + 194)<<16)+(31*p1.x + 23)这才是表示棋子相对窗口的坐标,有了这些参数,我们就可以编写代码实现消去一对棋子了,
BOOL click2p(POINT A,POINT B)
{
  
  int x = 23;
  int y = 193;  //第一格棋子坐标
  int LPARAM = ((y+35*A.y)<<16)+x+31*A.x;
  // TODO: Add your control notification handler code here
  HWND hwnd = FindWindow(NULL,GEAM_CAPTION);
  if (hwnd == NULL)
  {
    return FALSE;
  }
  SendMessage(hwnd,WM_LBUTTONDOWN,0,LPARAM);
  SendMessage(hwnd,WM_LBUTTONUP,0,LPARAM);
  LPARAM = ((y+35*B.y)<<16)+x+31*B.x;
  SendMessage(hwnd,WM_LBUTTONDOWN,0,LPARAM);
  SendMessage(hwnd,WM_LBUTTONUP,0,LPARAM);
  return TRUE;
}

  
6.  现在双击”单消”按钮,去到实现单消的成员函数,在这函数里,只要调用ClearPiar()函数即可,下图:
 
当然还要把头文件夹GeamProcess.h包含进去才能调用,下图:
 
现在单消的功能已经能够实现了
六、  修补与完善外挂
1.  既然已经实现了单消,那么实现全消也不是什么问题了,只要循环调用单消就行了,当然这个循环肯定要有个退出的条件,可以用剩余的棋子数作为退出的循环的条件,如图:
 
在棋盘下方的剩余方块的这个地方,就是表示棋盘上的棋子数,只要找出这个地址,然后读出这个地址的数据,我们还是用CE工具来查找,这个很容易找到,因为它是个可见的值,一直用精确值来查找就行了.找到之后就是编写代码读出数据,这跟之前读出棋盘数据差不多.只要把那段代码稍微改一下就好.
#define ChessNumAddre (LPVOID)0x001159FC  //棋子数基址
LPVOID chessnumber;
void UpChessNum()
{
  DWORD ProcID;
  HANDLE ProcHwnd;
  LPCVOID BackSize;
  HWND hwnd = FindWindow(NULL, GEAM_CAPTION);
  if (hwnd == NULL)
  {
    return;
  }
  GetWindowThreadProcessId(hwnd,&ProcID);
  ProcHwnd = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ProcID);
  ReadProcessMemory(ProcHwnd,ChessNumAddre,&chessnumber,1,(LPDWORD)&BackSize);
  CloseHandle(ProcHwnd);

上面的UpChessNum()函数跟之前的UpdateChess()函数几乎是一样的,只改动了几个地方(读取进程的地址、用来存放进程内容的变量、还有读取的大小).
我们把这个函数插入到实现消去棋子的click2p(POINT A,POINT B)函数里面去,(注意: UpChessNum()函数要定义在click2p(POINT A,POINT B)函数的前面)
 
这样只要每消去一对棋子,棋子数就会更新.现在可以编写”全消”按钮的成员函数代码了(双击”全消”按钮就可以去到该函数的定义).
void CQQllkWgDlg::OnExinction() 
{
  // TODO: Add your control notification handler code here
  UpChessNum();//更新棋子数
  while(chessnumber)
  {
    ClearPiar();
  }
}

函数中,一开始先更新棋子数,然后再进入while循环.
2.   
如上图,在游戏的过程中,还会碰到无解的情况,如果点击了”全消”按钮,在这种情况下,while循环就是一个死循环,程序会崩溃掉.解决这个问题,我们可以在游戏窗口中,点击重列的道具,我们也可以自动的模拟鼠标点击重列.首先用Spy++获取到重列图标相对窗口的坐标,(获取的方法,跟前面获取棋盘数组第一格的坐标相同,这里就不再叙述了)然而这个重列道具是有限的,还要用CE工具找到该地址(这个重列道具数也是可见的,它的地址也很容易找得到),然后读出重列道具数.

    #define RESTATED_NUM (LPVOID)0x001179C6   //重列地址
    LPVOID restatednumber;
VOID ReadRestated()
{
  DWORD ProcID;
  DWORD BackSize;
  HWND hwnd = FindWindow(NULL,GEAM_CAPTION);
  if (hwnd != NULL)
  {
    GetWindowThreadProcessId(hwnd,&ProcID);
    HANDLE ProcH = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ProcID);
ReadProcessMemory(ProcH,RESTATED_NUM,&restatednumber,1,&BackSize);
    CloseHandle(ProcH);
  }
}
  执行ReadRestated()函数就会在restatednumber返回重列道具数,
  再编写代码模拟鼠标点击重列图标.
    
VOID ReChess()  //重列棋盘
{
  LPARAM LPARAM = RESTATED_COORDINATE;
  HWND hwnd = FindWindow(NULL,GEAM_CAPTION);
  if (hwnd != NULL)
  {
    SendMessage(hwnd,WM_LBUTTONDOWN,0,LPARAM);
    SendMessage(hwnd,WM_LBUTTONUP,0,LPARAM);
  }
}

  
现在代码编写好了,要怎么样才能自动调用呢? ClearPiar()函数是实现消去一个棋子,它还是一个布尔类型的函数,当它的返回值是true时,表示当返回值是false时就表示没有消去棋子,返回false的情况只有两种,一是整个棋盘的棋子都消去了,没有棋子了,二是无解.那么我们就在ClearPiar()函数返回false的语句的前面,插入ReChess()函数,当然,不能直接插入,还要加一些判断.
  ReadRestated();  //读出重列道具数
  if (chessnumber && restatednumber)//如果棋子数与重列道具都不为零
  {
    ReChess();
  }

在ClearPiara()函数中插入以上代码,就可以实现自动重列了,如下图:
 
3.  现在再加上挂机速度跟自动挂机的功能,先把滑块的属性设置好,右键滑块--->Properties(属性)---->Styles(样式)--->钩上Tick marks(刻度标记)跟Enable selection(启用选择)还有Auto ticks(自动刻度)那个Point(点)的选项,随个人爱好选择,如下图:
 
然后再给滑块关联一个控件,使用控件调用类成员函数设置滑块的最小值跟最大值,以及初始值,还有刻度值.右键滑块--->ClassWizard(类向导)--->Member Variables(成员变量)--->双击滑块ID (IDC_SLIDER1),给滑块添加一个控件类型的变量.如下图:
 
在Category(类别)中选择Control(控件),写入变量名.点OK,如下图:
 
在以同样的方法添加一个数值类型的变量,如下图:
 
现在去初始化对话框的类中,添加设置控件信息的代码,在对话框空白处右键--->Events(事件)--->双击右边的初始化对话框消息,就能去到初始化对话框的成员函数,好下图:
 
在函数的尾部,返回值之前,添加上代码.
  m_Speed_Slider.SetRange(30,2000);//设置滑块范围
  m_Speed_Slider.SetPos(800); //设置初始位置
  m_Speed_Slider.SetTicFreq(100); //设置移动频率

以上三个函数,看字面意思就能大概明白意思.添加位置如下图:
 
滑块的基本信息设置好了,再给它添加一个编辑框,用来显示出当前滑块的数值(我给该编辑框关联了一个int型的变量m_SheepNum).然后再给滑块关联一个NM_RELEASEDCAPTURER 的消息响应(为什么是NM_RELEASEDCAPTURER消息,我也不是很清楚,找了一些资料,只朦胧的知道它能捕获鼠标抬起的消息,)右键滑块--->Events(事件)--->双击NM_RELEASEDCAPTURER--->OK.如下图:
 
再双击右边已经关联的NM_RELEASEDCAPTURER事件去到该成员函数的实现.如下图:
 
void CQQllkWgDlg::OnReleasedcaptureSlider1(NMHDR* pNMHDR, LRESULT* pResult) 
{
  // TODO: Add your control notification handler code here
  UpdateData(TRUE);//更新控件的值到变量
  m_SheepNum = m_SliderVar;
//m_SheepNum = m_Speed_Slider.GetPos();//此语句跟上面语句等价
  UpdateData(FALSE);//更新变量到编辑框
  *pResult = 0;
}

以上代码:当在滑块触发了鼠标抬起消息,用UpdateData(TRUE)函数更新滑块的数值到变量m_SliderVar中,然后再赋值给m_SheepNum,最后再用UpdateData(FALSE)函数把m_SheepNum的值更新到编辑框.
这样我们就能在编辑框上看到滑块的数值了(当移滑块的时候,是鼠标抬起了,才更新数值).下面再来实现把挂机速度跟滑块关联想来,当钩上”挂机速度”的时候,才能够移动滑块,如果不钩上”挂机速度”就不能移动滑块.
4.  给”自动挂机”复选框关联一个BOOL的变量m_GeamSpeed,然后双击复选框,去到成员函数的实现.
void CQQllkWgDlg::OnCheck2() 
{
  // TODO: Add your control notification handler code here
  UpdateData(TRUE);
  ::EnableWindow(m_Speed_Slider.m_hWnd,m_GeamSpeed); //该函数允许/禁止指定的窗口或控件接受鼠标和键盘的输入.
}

这现在再编译的话,就能用m_GeamSpeed变量的值控制滑块是否接收消息,还有一点没完整,就是一开始的时候, m_GeamSpeed默认是没钩上的,此时滑块还能接收鼠标的消息,这是因为这时没有触发调用void CQQllkWgDlg::OnCheck2()函数的消息,所以没起到理想的效果.解决问题很简单,只要把m_GeamSpeed变量初始化的值改为true就行了.右键点击m_GeamSpeed变量--->Go To Reference to m_GeamSpeed(转到参考),可以看到m_GeamSpeed变量初始化为false,把它改成true好了.如下图:
 
5.  实现自动挂机.同样的先给”自动挂机”复选框关联一个BOOL类型的变量(m_AutoGame),去到成员函数的实现.
void CALLBACK PlayProc(
            HWND hwnd, 
            UINT uMsg, 
            UINT idEvent, 
            DWORD dwTime 
            )//定义一个回调函数,
{
  ClearPiar();
}
CONST AUTOTIME = 111;//自动挂机定时器(全局变量)
void CQQllkWgDlg::OnCheck1() //自动挂机成员函数
{
  
  // TODO: Add your control notification handler code here
  UpdateData(TRUE);  //更新数据到变量
  if (m_AutoGame)
  {  
    SetTimer(AUTOTIME,m_SliderVar,&PlayProc);//创建一个定时器
  }
  else
  {
    KillTimer(AUTOTIME);
  }
}

以上代码先定义一个回调函数(实现单消功能),然后在void CQQllkWgDlg::OnCheck1()成员函数中建一个定时器,每隔一定的时间(m_SliderVar)就调用回调函数,从而实现自动挂机的功能.随着滑块的变动, m_SliderVar变量也跟着变动,这样挂机的速度也改变了,其实结果并不像我想的一样,测试的时候发现,当使用自动挂机时,滑动滑块,挂机速度并没有跟着改变,而是要去掉自动挂机重新钩上的时候速度才跟着改变,还要把代码改进一下.在滑块的成员函数中,我们添加一些代码.如下图:
 
当滑动滑块时,先关掉挂机定时器,然后再判断自动挂机变量,如果钩上了才打开.这样可以说比较完美的实现了自动挂机功能.
6.  现在再添加上去除游戏倒计时时间,用CE工具找出游戏倒计时地址(开始用大于0扫描,然后它是一直在减小的,所以一直用减少了的值扫描,直到时间跑完了,就用精确值为0扫描),然后再建立一个定时器,不断地把新时间写入到该地址(之前在我看的资料中,是用CE工具找到写入该地址的代码,然后把该代码去掉,这样我测试的时候,的确能实现,但是新版的qq游戏中,只要把这代码改了,就马上弹出警告,然后退出游戏,我一直也没找到解决这个问题的方法,如果有高人知道,希望能指点一二).这样游戏时间就不会减少了,再添加一个”去掉倒计时”的复选框,同时关联一个BOOL类型的变量m_ClearTime.
HANDLE ProcH;
#define COUNTDOWNTIME (LPVOID)0x00117584  //倒计时地址
CONST CLEARTIME = 112;//去除倒计时定时器
void CALLBACK CTProc(
             HWND hwnd, 
             UINT uMsg, 
             UINT idEvent, 
             DWORD dwTime 
            )
{
  DWORD BackSize;
  DWORD TimeNum = 750;//量大时间值
  WriteProcessMemory(ProcH,COUNTDOWNTIME,&TimeNum,4,&BackSize);写入时间
}
void CQQllkWgDlg::OnCheck3() 
{
  // TODO: Add your control notification handler code here
  UpdateData(TRUE);
  DWORD ProcID;
  
  HWND hwnd = ::FindWindow(NULL,GEAM_CAPTION);
  if (hwnd != NULL)
  {
    GetWindowThreadProcessId(hwnd,&ProcID);
    ProcH = OpenProcess(PROCESS_ALL_ACCESS,FALSE,ProcID);
    
    
  }
  if (m_ClearTime)
  {  
    SetTimer(CLEARTIME,100,&CTProc);
  }
  else
  {
    CloseHandle(ProcH);
    KillTimer(CLEARTIME);
  }
}

上面代码也是前面已经用过的,只是稍微改动一点,首先定义一个回调函数(实现写入时间750),后面就是打开窗口跟进程之类的,然后是判断复选框变量选择性的创建定时器,定时器是每隔100毫秒就调用一次回调函数.
7.  再添加一个自动开始游戏的功能,添加一个”自动开始”的复选框,同时关联一个BOOL类型的变量用Spy++工具取得游戏中”开始”按键的坐标,然后编写代码模拟鼠标点击开始.
CONST AUTOPLAY = 113; //自动开局定时器
void CALLBACK GamePlay(
           HWND hwnd, 
           UINT uMsg, 
           UINT idEvent, 
           DWORD dwTime 
            )
{
  LPARAM LPARAM = GAMEPLAY;
  hwnd = FindWindow(NULL,GEAM_CAPTION);
  if (hwnd != NULL)
  {
    PostMessage(hwnd,WM_LBUTTONDOWN,0,LPARAM);
    PostMessage(hwnd,WM_LBUTTONUP,0,LPARAM);
  }
}
void CQQllkWgDlg::OnCheck4() 
{
  // TODO: Add your control notification handler code here
  UpdateData(TRUE);  //更新数据到变量,
  if (m_AutoPlay && (chessnumber == NULL))
  {  
    SetTimer(AUTOPLAY,1000,&GamePlay);
  }
  else
  {
    
    KillTimer(AUTOPLAY);
  }
}

定义一个回调函数(实现模拟鼠标点击”开始”),值得注意的是这时候我是用PostMessage函数来实现模拟鼠标,而不是像自动重列一样使用SendMessage函数,这是因为使用SendMessage函数的时候,不能实现模拟点击(可能是QQ游戏已经在那个地方屏蔽掉SendMessage的消息了),改用PostMessage函数的时候就没有问题了.当钩上了自动开局棋子数为0,就创建一个时间段为1000毫秒的定定时器.

上传的附件 image001.png
image003.png
image005.png
image007.png
image009.png
image011.png
image013.png
image015.png
image017.png
image019.png
image021.png
image023.png
image025.png
image027.png
image029.png
image031.png
image033.png
image035.png
image037.png
image039.png
image041.png
image043.png
image045.png
image047.png
image049.png
image051.png
image053.png
image055.png
image057.png
image059.png
image061.png
image063.png
image065.png
image067.png
image069.png
image071.png
image073.png
image075.png
image077.png
image079.png
image081.png
image083.png
image085.png
image087.png
image089.png
image091.png
image093.png
image095.png
image097.png
image099.png
image101.png
image103.png
image105.png
image107.png
image109.png
image111.png
image113.png
image115.png
image117.png
image119.png
image121.png
image123.png
image125.png
image127.png
image129.png
image131.png

注意:上传附件及图片大小不得大于30M。

⚠️ 版权声明:
本博客所有内容(含教程、源码、工具)仅供个人技术学习与研究交流使用,严禁商用、倒卖、二次分发及非法用途。
未经作者书面授权,任何组织或个人不得转载、复制或用于其他平台,违者将追究相关责任。

0 0 0 举报
复制成功