郁金香VC++教程及手记
初级篇1.1.1教学目标:模拟鼠标操作 1.1.1、游戏数据分析(SPY++) a、取得窗口相对坐标
b、读出游戏窗口信息GetWindowRect c、移动鼠标指针SetCursorPos HWND FindWindow(
LPCTSTR lpClassName, //窗口类名 LPCTSTR lpWindowName //窗口标题 );
教学过程:
取游戏标题:QQ游戏 - 连连看角色版
取开局所在坐标:x=655;y=577 //lparam 0x0241028f 拦截消息:WM_LBUTTONDOWN,WM_LBUTTONUP
API-FindWindow(NULL,\游戏 - 连连看角色版\
打开VC,新建-工程-MFC EXE-工程名是LLKWG,然后选择基本对话框,完成即可 [attachment=535]
还要在编辑框中关联变量 ,建立类向导 [attachment=536]
在游戏开局按钮输入代码
// The system calls this to obtain the cursor to display while the user drags // the minimized window.
HCURSOR CLlk_wgDlg::OnQueryDragIcon() {
return (HCURSOR) m_hIcon; }
HWND gameh;//游戏窗口句柄
RECT r1; // RECT结构表示一个矩形区域 void CLlk_wgDlg::*****tartGame() {
// TODO: Add your control notification handler code here
gameh=::FindWindow(NULL,\游戏 - 连连看角色版\获取游戏窗口句柄 ::GetWindowRect(gameh,&r1); //这里取坐标, 双冒号是全局的意思
this->m_x=r1.left;this->m_y=r1.top;//读出窗口左上角坐标, this是关联变量 UpdateData(false); //显示到编辑框
//设置鼠标指针位置 取开局所在坐标:x=655;y=577 //lparam 0x0241028f SetCursorPos(655+r1.left,577+r1.top);//当前窗口坐标+开局按钮坐标 }
1.1.2 用VC++写个最简单的外挂(实现游戏开局) a、鼠拟鼠标单击mouse_event b、鼠标指针移动还原
c、集成到startgame函数里 教学过程:
//模拟鼠标的单击(鼠标按下/鼠标抬起) //鼠标在当前位置按下
mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); //鼠标在当前位置抬起
mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); 小结:
mouse_event,Sleep,SetCursorPos
这次用到了鼠标点击的函数mouse_event鼠标硬件模拟,如果调用不成功则延时一下Sleep (200),然后再将鼠标移回原位SetCursorPos这个是位置设置函数
调用成功后,将函数放在一个.h头文件里,方便以后调用.新建 c/c++ Header File,文件名GameProc #include \//游戏 功能函数 HWND gameh; RECT r1;
POINT p;//x,y void startGame() {
// TODO: Add your control notification handler code here //获取游戏窗口句柄
gameh=::FindWindow(NULL,\游戏 - 连连看角色版\ ::GetWindowRect(gameh,&r1); //保存当前鼠标指针 //取得当前鼠标位置 GetCursorPos(&p);
1
//设置鼠标指针位置 取开局所在坐标:x=655;y=577 //lparam 0x0241028f SetCursorPos(655+r1.left,577+r1.top); //模拟鼠标的单击(鼠标按下/鼠标抬起)
mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); //鼠标在当前位置按下 mouse_event(MOUSEEVENTF_LEFTDOWN,0,0,0,0); //鼠标在当前位置抬起 mouse_event(MOUSEEVENTF_LEFTUP,0,0,0,0); Sleep(200);//过一段时间 再执行后边的代码 SetCursorPos(p.x,p.y); //还原鼠标位置 }
当然还要将这个.h文件包涵进主函数里 #include \#include \#include \#include \然后这样调用
void CLlk_wgDlg::*****tartGame() {
startGame(); }
1.2.1、CE中的数据类型
a、数据类型:Bit,Byte,Word,Dword,float,double b、用CE查找出坐位号; c、保存分析数据 教学过程:
a、数据类型:Bit,Byte,Word,Dword,float,double C++数据类型
bit/位 1位 取值范围:0..1
byte(字节)=0..11111111(2进制)=0..255(10进制)=0..FF (16进制) WORD(单字)=2Byte=0..65535(10进制) =0..FFFF (16进制) DWORD(双字)=2WORD=4Byte=0..4294967295 =0..FFFFFFFF float(浮点数) double(双浮点数)
int(4字节),long(4字节),WORD,DWORD(4字节),float(4字节),double(8字节) b、用CE查找出坐位号; 打开 QQ游戏 连连看 猜测 0..5,1..6,顺时针 0..7
坐位号地址:0x00B8D8E0
因为游戏当中不同的座位号数据不一样,所以首先要找到座位号.
用CE搜索游戏是字节型来查找游戏的座位号,一桌是6个位置,猜测是从0到5,上面是0,顺时针方向加1.所以我们更换座位号后再继续用CE搜索相应的位置号,注意附加的进程可不要出错哦.QQ连连看的进程名是KYODAI~1.EXE,可以用任务管理器找出. 找到后将CE的数据保存一下,方便下次调用. 1.2.2、编程读出坐位号;
a、远程读取进程数据 b、打开远程进程 c、读取远程进程数据 教学过程: API函数介绍
1、FindWindow //获取窗口句柄 2、GetWindowThreadProcessId //获取窗口进程ID 3、OpenProcess //打开指定进程
4、ReadProcessMemory //读指定进程内存数据 游戏进程名:KYODAI~1.EXE
游戏窗口标题:\游戏 - 连连看角色版\ HWND FindWindow(
LPCTSTR lpClassName, // NULL 忽略 LPCTSTR lpWindowName // 窗口标题 );
BOOL ReadProcessMemory(
HANDLE hProcess, // 进程句柄 LPCVOID lpBaseAddress,
// 基址0x00B8D8E0 LPVOID lpBuffer, // 存放数据缓冲区 DWORD nSize, // 要读取数据的字节数 LPDWORD lpNumberOfBytesRead
// 实际读取的字节数 );
这次需要几个函数来取得窗口/句柄/进程/内存等信息
2
几个函数要联合运用,前一个函数的返回值就是后一个函数的参数
在VC代码里添加座位号的变量m_Num 类型是UINT,新增一个按钮,添加如下代码 [attachment=537]
c*****t PCHAR gameCaption=\游戏 - 连连看角色版\void CLlk_wgDlg::OnButton2() {
// 游戏窗口标题:\游戏 - 连连看角色版\ // 1、FindWindow //获取窗口句柄 //2、GetWindowThreadProcessId //获取窗口进程ID //3、OpenProcess //打开指定进程
//4、ReadProcessMemory //读指定进程内存数据 //获取窗口句柄
HWND gameh=::FindWindow(NULL,gameCaption); //获取窗口进程ID DWORD processid;
::GetWindowThreadProcessId(gameh,&processid); //打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid); //读指定进程内存数据 DWORD byread;
LPCVOID pbase=(LPCVOID)0x00B8D8E0;//读取当前的指针,强制转换为LPCVOID指针 LPVOID nbuffer=(LPVOID)&m_num; //保存当前的指针,强制转换为LPVOID指针 ::ReadProcessMemory(processH,pbase,nbuffer,4,&byread); UpdateData(false); //更新变量的值到 编辑框 }
1.2.3、用CE查出棋盘基址; a、找棋盘数据基址 b、分析棋盘数据结构 19宽*11高 :数组 byte a[19][11] byte 0..255 // 00..FF
0x0012A508 //棋盘数组基址
db 地址 // 以字节方式 显示指定地址内存里的数据 前面座位号的基址已经找出了,所以这次要找出棋盘的数据,
要先查一下棋盘的2维排列,是19宽*11高,用QQ截图查找一下长和宽,然后计算出每一格的大小.猜测棋盘的棋子也是字节的,我们查找左上角第一棋子的数据.
用CE查找,如果有棋子就是大于0,变化了就再搜索更改的数值,没有棋子就是0,多次查找就找到第一棋子的地址了.自己找个座位坐下来,加个密码不让别人进,多按几次\练习\按钮,这样棋盘变化就方便找棋子数据了. 老师也查找出错了,再来一次吧. 这次找到了..
找到后用OD加载一下看看,这样的数据排序看的比较清楚 1.2.4、读出当前棋盘数据
a、编程读出棋盘数据
b、棋盘数据显示出来 参考章节:1.2.3,1.2.2
19宽*11高 :数组 byte a[11][19] // a[y][x] byte 0..255 // 00..FF
0x0012A508 //棋盘数组基址
db 地址 // 以字节方式 显示指定地址内存里的数据 itoa(要转换的整数,存放字符数组,要转换的字符进制) for (int y=0;y<=10;y++) //y++ y:=Y+1;
添加了一个编辑框用来显示棋盘数据,再关联一个变量m_Chessdata Cstring类型 [attachment=538]
增加”更新棋盘数据”按钮,添加代码如下: byte chessdata[11][19];//a[y][x] void CLlk_wgDlg::OnBtnReadchess() {
// TODO: Add your control notification handler code here //获取窗口句柄
HWND gameh=::FindWindow(NULL,gameCaption); //获取窗口进程ID DWORD processid;
::GetWindowThreadProcessId(gameh,&processid); //打开指定进程
HANDLE processH=::OpenProcess(PROCESS_ALL_ACCESS,false,processid); //读指定进程内存数据 DWORD byread;
LPCVOID pbase=(LPCVOID)0x0012A508; //棋盘数据基址 LPVOID nbuffer=(LPVOID)&chessdata; //存放棋盘数据
3
::ReadProcessMemory(processH,pbase,nbuffer,11*19,&byread); char buf[11]; ///显示棋盘数据 m_chessdata=\先清空编辑
for (int y=0;y<=10;y++)//一列一列的读,FOR循环:Y=0是循环的起始值,Y<=10是终止值,Y++是每次Y+1 {
for (int x=0;x<=18;x++) //一行一行的读 {
itoa(chessdata[y][x],buf,16); //itoa整型转换成字串 m_chessdata+=buf; m_chessdata+=\ }
m_chessdata+=\换行 }
UpdateData(false); //更新数据 }
这节课讲了VC编程,注意细节,数据是按照[Y][X]排序的,注意棋盘数据是2位数的,要将char buf改成char buf[11]. 1.3.1 分析棋子与棋盘坐标关系
a、鼠标软件模拟,函数SendMessage b、分析窗口内棋子相对坐标X,Y
c、软件模拟点击棋盘坐标x,y处的棋子 1、SendMessage;
SendMessage(hwnd,WM_LBUTTOMDOWN,0,YX);//hwnd=FindWindow(NULL,游戏标题); SendMessage(hwnd,WM_LBUTTOMUP,0,YX); //PostMessage/mouse_event 2、获取棋盘左上角棋盘第一格坐标. 棋盘第一格 坐标 x=21,y=192 int x=22,y=187;
hwnd=FindWindow(NULL,游戏标题);
SendMessage(hwnd,WM_LBUTTONDOWN,0,(y<<16)+x);// SendMessage(hwnd,WM_LBUTTONUP,0,(y<<16)+x); // 3、计算棋盘的 宽度*高度 589*385 棋盘第一格 坐标 x=21,y=192
31*35 棋子宽度,高度
SendMessage(hwnd,WM_LBUTTONDOWN,0,(y<<16)+x+31*2);// SendMessage(hwnd,WM_LBUTTONUP,0,(y<<16)+x); //
//SendMessage 鼠标模拟,//WM_LBUTTONDOWN 鼠标左键按下 //WM_LBUTTONUP 鼠标左键抬起 //<< 左移指令
前面都是直接移动了鼠标,这次要改发送鼠标消息了,这样鼠标不移动也会点击游戏的开始按钮.SendMessage的参数是相对坐标, mouse_event的参数是绝对坐标
再次打开SPY++,找到棋盘第一格的位置,X=21,Y=187
新增一个按钮”点击棋盘第一格”方便测试,添加代码如下:
void CLlk_wgDlg::OnButton3() //按钮函数 {
int x=22,y=187; //定义座标点 HWND hwnd=::FindWindow(NULL,gameCaption); //查找窗口 int lparam; //定义座标点变量
lparam=(y<<16)+x+31*2; //表示指定格,Y<<16是左移16位,发消息用的Y座标点 ::SendMessage(hwnd,WM_LBUTTONDOWN,0,lparam);//鼠标按下消息 ::SendMessage(hwnd,WM_LBUTTONUP,0,lparam); //鼠标抬起消息 }
用QQ抓图,查找棋子格子数大小,31*35的大小,以便计算出所有格子的座标点. 1.3.2 消掉一对棋子的算法框架
a、遍历棋盘同类型棋子配对 b、构建算法框架 //遍历整个 棋盘 找相同一对棋子 //检测这一对棋子是否 可以消除
//如果 可以消除 ,则模拟鼠标点击这2点
最多一条 三条 连线 能够形式一条 路线 就表示可消除 更改”点击棋盘第一格”代码如下: void ClearPiar() //消除一对棋子 {
//读出棋盘数据至chessdata 11,19 updatdChess();
//遍历整个棋盘 找出相同类型 一对棋子 POINT p1,p2;//定义两个点,座标型 int x1,y1,x2,y2;//定义座标点
4
for (y1=0;y1<11;y1++)//点1循环,从Y列开始 for (x1=0;x1<19;x1++)//点2循环,从X行开始
{ for (y2=y1;y2<11;y2++)//这是点2的Y循环,由Y1开始 for (x2=x1;x2<19;x2++)//这是点2的X循环,由X1开始
// 棋子1与棋子2 类型是否相同, 要求点1与点2 相等则假,就是座标不能相同.
if ((chessdata[y1][x1]==chessdata[y2][x2]) &&(!((x1==x2)&&(y1==y2))) ) {
p1.x=x1;p1.y=y1; p2.x=x2;p2.y=y2;
//检测 相同的2个棋子是否可消掉
if ( check2p(p1,p2))//如果可消除 则返回真 {
//click2p 鼠标模拟 点击 p1,p2 click2p(p1,p2); } } } }
1.3.3 (Check2p)大致框架(算法核心)
a、在这一对棋子间找相通路径的原理 b、(Check2p函数)框架代码
c、(CheckLine函数)检测2点是否有连通
LineNull(p1,p2); //是否在棋盘上 的2个点之前是否有一条全为0的直线,如有true,否则false 1、剪贴游戏图;
Y坐标相同的情况下 p1,p2
lineNull(p1.right,p2.left) //可消除 X坐标相同的情况下 p1,p2
LineNull(p1.down,p2.up) //可消除 X与Y坐标都不相情况下 p1,p2
lineNll(p1.down,pa),LineNull(p2.down,pb),LineNull(pa,pb) //可消除
现在就要来分析游戏了,连连看大家都知道,是需要判断两点间是否可以连接的,比如有直连,有一折后的连接,最多是二折后的连接.
如果是直连的话,就要判断中间是否所有的数据都为0,判断的思路很主要,一定要搞清楚.而有折的连接则需要多条直线,最多是三条直线,这个需要遍历,不断的向下判断.
将VC代码打开,插入一个类Generic Class,名称为CChessPoint 添加代码如下:
class CChessPoint {
public:
POINT p;//临时点 POINT up;//上点 POINT down;//下点 POINT left;//左点 POINT right;//右点
CChessPoint(POINT pxy); //构造函数 virtual ~CChessPoint(); };
还要在Cchesspoint.cpp实现部分修改代码 CChessPoint::CChessPoint(POINT pxy)
{ up=pxy;down=pxy;left=pxy;right=pxy;//将座标初始化 //向上下左右扩展 p=pxy;
up.y=pxy.y-1; down.y=pxy.y+1; left.x=pxy.x-1; right.x=pxy.x+1;
//这样处理完之后每个棋子就包涵了上下左右中五个点的属性 }
CChessPoint::~CChessPoint() { }
在按钮部分增加代码如下:
bool lineNull(POINT p1,POINT p2) {
return true;//先写个空的,下节课继续. }
bool check2p(POINT p1,POINT p2)
5