caothiengthien
14-06-11, 09:51 AM
tình cờ dạo trên box <<b><font color=red>[Chỉ có thành viên mới xem link được. <a href="register.php"> Nhấp đây để đăng ký thành viên......</a>]</font></b>> thấy cái này hay hay nên post cho mọi người tham khảo<chủ yếu là newber>
Bài 1 – Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ
Bài 3 – Điều khiển nhân vật chạy
Ở đây không bàn đến việc dùng kỹ thuật đồ họa và chiếm chuột để điều khiển nhân vật. Các phần mềm hiện nay thường dùng chương trình chính Autoplay thực hiện việc đọc bộ nhớ Game lấy thông tin và dùng cơ chế Hook để ghi thông tin cần thay đổi vào bộ nhớ Game. Một điểm lưu ý là khi Hook sẽ chiếm tài nguyên, nên ta có thể dùng một mẹo nhỏ, sau khi Hook ta Unhook ngay lập tức và dùng LoadLibrary để nạp hook.dll vào bộ nhớ game, thay đổi địa chỉ WndProc để thực hiện nhiều ý đồ khác.. Ngoài ra, để chương trình có thêm nhiều chức năng, có thể ta sẽ phải gọi các hàm trong game_y.exe và khi đó ta phải mở rộng cái Hook.dll để có thể hook + inject code vào Game process (sẽ nói trong bài 3).
Bài 1 - Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Tất cả các nhân vật (người chơi, quái, Npc...) được lưu thông tin trong mảng gồm 256 đối tượng, mỗi đối tượng kích thước 0x7E4C, địa chỉ lưu địa chỉ của mảng trên là 0x00D3A570. Đối tượng 0 để trống, đối tượng 1 là người chơi, còn lại là quái & Npc.
Khi chưa dùng cơ chế inject code nói trên ta có thể đọc bộ nhớ của Game bằng hàm ReadProcessMemory. Dưới đây là minh họa lớp đối tượng Npc và một số biến thành viên cần dùng (phần ... là phần dữ liệu chưa dùng tới, các bạn phải đặt một mảng BYTE với kích thước tương ứng vào đó để các địa chỉ OFFSET của các biến được chính xác)
CODE
#define NPC_BASE_ADD 0x00D3A570
#define NPC_DATA_SIZE 0x00007E4C
#define MAX_NPC 256
#define PLAYER_INDEX 1
class CNpc
{
...
DWORD m_NpcKind; //0x0020 1=mod, 1=player,...
...
DWORD m_Doing; //0x00EC 1=stand, 2=walk, 3=run...
...
int m_CurLife; //0x0B44
int m_CurLifeMax;
int m_CurLifeRep;
int m_CurMana;
int m_CurManaMax;
...
BOOL m_bRideHorse; //0x0D58
char Name[32]; //0x0D5C Ten nhan vat
int m_nSex;
...
BOOL m_FightMode; //0x0EAC
};
// Tim cua so va xac dinh Game Process Handle o day
...
CNpc Npc;
LPBYTE lpBaseAdd, lpCurAdd;
//Read start Address of Npc Array
ReadProcessMemory(m_hVLProcess, (LPBYTE)NPC_BASE_ADD, (LPVOID)&lpBaseAdd, 4, NULL);
//Start Address of PLAYER
lpCurAdd = lpBaseAdd + PLAYER_INDEX*NPC_DATA_SIZE;
//Read PLAYER ìnormation
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
...
Khi đọc được các thông số máu và mana rồi ta có thể dùng hàm PostMessage(hWnd, WM_KEYDOWN...) để gửi phím 1, 2, 3. Các bạn thấy ở trên có 1 biến m_FightMode dùng để xác định nhân vật đang ở chế độ đánh (ở bản đồ có quái) hoặc đang ở trong thành (không dùng chiêu được). Căn cứ vào đó ta có thể gửi phím 1, 2, 3 khi cần thiết (m_FightMode=TRUE). Nếu nhân vật về thành rồi khỏi bơm máu nữa (nhiều khi dính độc về cả phút mà cứ đứng trong thành bơm máu thì phí quá).
Linh tinh khác: Game VLTK có định nghĩa một số hàm macro có thể dùng ở dòng gõ CHAT, ví dụ:
“/Switch Horse”, “/Switch Sit”, “/SayPhrase 0”, , “/SayPhrase 1” các bạn thử mà xem, cũng thũ vị lắm. Các bạn mở file UIconfig.ini sẽ thấy nhiều hàm hơn. Dưới đây là đoạn code mà có thể thêm vào chương trình của mình dùng để chat hoặc chạy một hàm macro trong Game. Bạn có thể lập trình lên xuống ngựa hay ngồi xuống, đứng lên, ... mà ko phải dùng WM_KEYDOWN gửi phím tắt V, M (vì ở chế độ phím Mặc định, nhấn V sẽ ko ngồi đâu).
CODE
void PostChatMessage(LPCTSTR szChatMsg)
{
//Set focus to CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
//Clear CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_DOWN, 0x00500001);
while (szChatMsg[0])
{
::PostMessage(m_hVLWin, WM_CHAR, LOBYTE(szChatMsg[0]), 0);
szChatMsg++;
}
//Set focus to Game Window
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
}
Đến đây các bạn có thể lập được một chương trình tự buff mana, ngồi rao bán hàng rồi đấy. Ở bài 2 tôi sẽ nói về cách xác định tọa độ nhân vật (cũng giống như của các Npc khác) và đồ. Địa chỉ OFFSET tọa độ của nhân vật & Npc không phải là 0x10F8 như các bạn tưởng đâu, đó là địa chỉ đích Npc sẽ chạy tới, sẽ đánh tới... nếu nhân vật ngồi xuống, giá trị này sẽ bằng 0.
Bây giờ đến phần toạ độ của nhân vật. Bổ sung thêm các biến thành viên vào lớp CNpc, chúng ta có các thông tin về toạ độ và thông tin đối tượng cần đánh/ theo sau/ đồ cần nhặt:
CODE
class CNpc
{
public:
...
int m_NextAdd; //0x000C – xac dinh Npc ton tai hay ko
...
int m_ActiveSkill; //0x0114 – ky nang dang su dung
...
int m_MapX, m_MapY, m_MapZ; //0x0D0C – toa do Cell
int m_OffX, m_OffY; //0x0D18 – toa do Npc trong Cell (*1024)
...
int m_nPeopleIdx; //0x0DE0 Npc can tan cong, hoac theo sau
...
int m_nObjectIdx; //0x0DEC Obj can nhat
...
WORD m_RegX; //0x0EC4 – toa do Region
WORD m_RegY;
public:
void GetMapPos(int *nX, int *nY);
};
Toàn bàn đồ được chia thành nhiều vùng, có kích thước 512x1024, mỗi vùng lại chia thành nhiều cell, có kích thước là 32x32. Hai biến WORD RegX, RegY lưu giá trị vùng (Region), các biến MapX, MapY lưu giá trị của cell; OffX,OffY là toạ độ Npc trong ô đó (giá trị = 1024/pixel).
Toạ độ của nhân vật được tính như sau:
CODE
#define REGION_WIDTH 512
#define REGION_HEIGHT 1024
#define CELL_WIDTH 32
#define CELL_HEIGHT 32
void CNpc::GetMapPos(int *nX, int *nY)
{
*nX = m_RegX*REGION_WIDTH + m_MapX*CELL_WIDTH + (m_OffX>>10);
*nY = m_RegY*REGION_HEIGHT + m_MapY*CELL_HEIGHT + (m_OffY>>10);
}
Hàm sẽ trả về nX, nY giá trị toạ độ tuyệt đối của nhân vật/ Npc trên bản đồ, tính theo pixel (không phải toạ độ vẫn thường nói trong GAME đâu). Còn toạ độ trong Game (thể hiện trên bản đồ nhỏ) sẽ được tính bằng:
CODE
nOrgX = nX>>8; // dich phai 8 bit, chia cho 256
nOrgY = nY>>9; // dich phai 9 bit, chia cho 512
Đến đây các bạn có thể xác định được toạ độ của nhân vật cũng như của Npc bất kỳ trong mảng 256 Npc rồi, và do đó sẽ xác định được khoảng các giữa chúng. Vấn đề là khi quái bị chết, không phải tất cả dữ liệu trong đối tượng bị xoá, trừ khi có quái hoặc player khác được nạp vào vùng nhớ đó. Tôi có thấy 1 địa chỉ ở OFFSET 0x000C, tạm đặt tên biến là m_NextAdd vì thấy nó lưu các địa chỉ quanh quanh trong mảng 256 Npc, khi quái chết, hoặc ra khỏi vùng nhớ (đi ra xa), giá trị m_NextAdd được đặt về 0..
Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ
ây giờ đến phần toạ độ của nhân vật. Bổ sung thêm các biến thành viên vào lớp CNpc, chúng ta có các thông tin về toạ độ và thông tin đối tượng cần đánh/ theo sau/ đồ cần nhặt:
CODE
class CNpc
{
public:
...
int m_NextAdd; //0x000C – xac dinh Npc ton tai hay ko
...
int m_ActiveSkill; //0x0114 – ky nang dang su dung
...
int m_MapX, m_MapY, m_MapZ; //0x0D0C – toa do Cell
int m_OffX, m_OffY; //0x0D18 – toa do Npc trong Cell (*1024)
...
int m_nPeopleIdx; //0x0DE0 Npc can tan cong, hoac theo sau
...
int m_nObjectIdx; //0x0DEC Obj can nhat
...
WORD m_RegX; //0x0EC4 – toa do Region
WORD m_RegY;
public:
void GetMapPos(int *nX, int *nY);
};
Toàn bàn đồ được chia thành nhiều vùng, có kích thước 512x1024, mỗi vùng lại chia thành nhiều cell, có kích thước là 32x32. Hai biến WORD RegX, RegY lưu giá trị vùng (Region), các biến MapX, MapY lưu giá trị của cell; OffX,OffY là toạ độ Npc trong ô đó (giá trị = 1024/pixel).
Toạ độ của nhân vật được tính như sau:
CODE
#define REGION_WIDTH 512
#define REGION_HEIGHT 1024
#define CELL_WIDTH 32
#define CELL_HEIGHT 32
void CNpc::GetMapPos(int *nX, int *nY)
{
*nX = m_RegX*REGION_WIDTH + m_MapX*CELL_WIDTH + (m_OffX>>10);
*nY = m_RegY*REGION_HEIGHT + m_MapY*CELL_HEIGHT + (m_OffY>>10);
}
Hàm sẽ trả về nX, nY giá trị toạ độ tuyệt đối của nhân vật/ Npc trên bản đồ, tính theo pixel (không phải toạ độ vẫn thường nói trong GAME đâu). Còn toạ độ trong Game (thể hiện trên bản đồ nhỏ) sẽ được tính bằng:
CODE
nOrgX = nX>>8; // dich phai 8 bit, chia cho 256
nOrgY = nY>>9; // dich phai 9 bit, chia cho 512
Đến đây các bạn có thể xác định được toạ độ của nhân vật cũng như của Npc bất kỳ trong mảng 256 Npc rồi, và do đó sẽ xác định được khoảng các giữa chúng. Vấn đề là khi quái bị chết, không phải tất cả dữ liệu trong đối tượng bị xoá, trừ khi có quái hoặc player khác được nạp vào vùng nhớ đó. Tôi có thấy 1 địa chỉ ở OFFSET 0x000C, tạm đặt tên biến là m_NextAdd vì thấy nó lưu các địa chỉ quanh quanh trong mảng 256 Npc, khi quái chết, hoặc ra khỏi vùng nhớ (đi ra xa), giá trị m_NextAdd được đặt về 0.
Vậy thuật toán đơn giản để tìm quái để tấn công sẽ như sau:
CODE
CNpc Player; Npc;
int nX, nY;
int nMinIndex, nMinDistance=2000;
//Read PLAYER information
lpCurAdd = lpBaseAdd + NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Player, sizeof(CNpc), NULL);
for (int i=2; i<MAX_NPC; i++)
{
//Read NPC information
lpCurAdd = lpBaseAdd + i*NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
if (!Npc.m_NextAdd) continue; // Npc does not exist
if (Npc.m_CurLife<=0 || Npc.m_NpcKind != 0) continue;
//Xac dinh toa do quai
Npc.GetMapPos(int &nX, &nY);
//Tinh khoang cach den nhan vat
int nDistance = ...
//Kiem tra xem khoang cach la nho nhat de tan cong truoc
if (nDistance<=nMinDistance)
{
nMinDistance = nDistance;
nMinIndex = i;
}
}
//Player.m_nPeopleIdx = nMinIndex; // Tan cong quai o gan nhat
SendMessage(WM_HOOK_WRITE,...); // Ghi gia tri vao bo nho Game thong qua Hook
Vòng for trên tôi viết đơn giản hoá để dễ hiểu, các bạn có thể viết thêm các chức năng kiểm tra lag (sau khi đánh một lúc mà quái ko mất máu thì đổi con khác). Có 1 lưu ý nhỏ ở đây là khi máu quái = 0 nó vẫn sống, < 0 nó mới chết nên trong việc kiểm tra đánh quái, các bạn đánh đến khi máu < 0 thì mới search tiếp con khác. Đó là việc ở trước vòng for này, còn trong vòng for ở trên vì sao tôi vẫn kiểm tra Npc.m_CurLife<=0 thì bỏ qua Npc, các bạn có thể tự tìm hiểu nhé.
Khi các bạn đã có thể lấy toạ độ quái thì nên chọn thuật toán đánh quái ở gần trước. Theo tôi mình ko nên chọn phương pháp đánh quái ít máu trước vì sẽ bị KS hết điểm kn và nhân vật phải chạy tới chạy lui tít xa, không tối ưu.
Tiếp theo ta sẽ nghiên cứu mảng lưu thông tin đồ rớt ra. Mảng này gồm 256 đối tượng, nằm bắt đầu ở địa chỉ tĩnh 0x00DC2750, kích thước mỗi đối tượng là 0x03C4. Ta chỉ cần lấy phần dữ liệu ở đầu đối tượng và định nghĩa thành lớp CObj của chúng ta như dưới đây.
CODE
class CObj //dung nham lan voi CObject cua VC++ day nhe
{
public:
int m_nID; // -1 la do ko ton tai, > 0 la ton tai
int m_nData1; // Unused data
int m_ObjKind; // 3=item, 4=money,..10=Property Box
int m_Index;
WORD m_RegX, m_RegY; // Toa do Region
int m_nData2[2]; // Unused data
int m_MapX, m_MapY; // 0x001C, toa do Cell
int m_OffX, m_OffY; // 0x0024, toa do Off trong Cell
int m_nData3[13]; // Unused data
char m_szName[32]; // 0x0060
int m_nData4; // Unused data
int m_nColor; // Mau cua do (0=trang, 3=xanh, ?=vang...)
int m_nData5[3]; // Unused data
int m_nMoneyNum; // Luong tien
public:
void GetMapPos(int *nX, int *nY);
};
Chặc, đến đây các bạn lại thấy quá dễ để tính tọa độ của đồ rồi đúng ko, cũng theo cách tính như ở CNpc thôi. Nhặt đồ thì dễ hơn đánh quái nhiều, tìm cái đồ nào ở gần thì nhặt. Khoảng cách có thể nhặt đô trong Game đặt là 64, nhưng nếu cần các bạn cứ tìm trong khoảng 200-300 gì đó, nhân vật chỉ cần nhích đi một chút là nhặt được đồ. Các bạn còn nhớ cái biến thành viên m_nObjectIdx trong lớp CNpc ko? Nó sẽ xác định địa chỉ cần gán cho đối tượng người chơi để nhặt đồ (cũng theo vị trí của đồ trong mảng 256 đối tượng kia)
Bổ sung: Lần đầu tiên khi nhân vật bắt đầu đánh, các bạn phải gán chiêu thức vào m_ActiveSkill ở OFFSET 0x0114 nhé (tham khảo thêm bài viết của TinhLaGi). Ngoài ra ở phần trên tôi có bỏ sót một biến thành viên tại địa chỉ Offset 0x0C58, đó là m_nSkillRadius - bán kính hiệu quả của chiêu thức. Trước khi gán lệnh đánh, bạn gán giá trị này một số nhỏ, ví dụ 75 chẳng hạn là bán kính quyền cơ bản, nhân vật sẽ tiếp sát rồi mới đánh quái. Nếu ko để chế độ đánh tiếp cận, sau khi đánh 1 chiêu có bán kính hiệu quả = 0 (ví dụ NLTD), các bạn lại phải gán lại số này đủ lớn, nếu ko nhân vật sẽ chạy lại sát quái mà vẫn ko với tay đánh được. Có thể gán = 75 hoặc thậm chí = 5000, game sẽ tự reset lại.
Thêm nữa, phần đọc tọa độ của nhân vật, Npc & Obj có một số bạn dùng địa chỉ offset 0x0D20, 0x1088.. gì đó. Thực ra các địa chỉ này chỉ là kết quả của các tính toán hoặc gán tham số tọa độ cho các thao tác của nhân vật nên đôi khi mình đọc ra sẽ thu được một kết quả không chính xác.
Bài viết tiếp theo tôi sẽ đề cập đến việc điều khiển nhân vật chạy đến một tọa độ cho trước
Bài 3 – Điều khiển nhân vật chạy
Nào hôm nay chúng ta sẽ nghiên cứu đến cái việc điều khiển quái chạy. Dưới đây là các biến thành viên bổ sung cho lớp CNpc:
CODE
class CNpc
{
public:
...
int CmdKind; //0x1088, 2=walk, 3=run
int Param_X;
int Param_Y;
};
Để cho nhân vật chạy hoặc đi bộ đến 1 điểm nào đó, các bạn gán các giá trị thích hợp vào các địa chỉ trên, bao gồm loại lệnh (chạy, đi bộ) và tọa độ X, Y.
SendMessage(WM_HOOK_WRITE, ...3);
SendMessage(WM_HOOK_WRITE, ...nX);
SendMessage(WM_HOOK_WRITE, ...nY);
Tuy nhiên, công việc ko chỉ dừng lại ở đó, nếu ko nhân vật sẽ tự giật lại vị trí cũ. Bước tiếp theo là chúng ta phải gửi một gói tin đến server, báo rằng nhân vật di chuyển đến tọa độ mới. Như các bạn biết, đôi khi mạng lag, ta điều khiển nhân vật chạy được một đoạn nó lại giật lại chỗ cũ là vì gói tin báo đã ko gửi đi được.
Để làm được việc đó ta sẽ viết code gọi hàm gửi tin đến server. Sau khi nghiên cứu, tôi đã tìm được địa chỉ các hàm gửi lệnh RUN và hàm gửi lệnh WALK trong game_y.exe. Cũng may là mấy hàm này ko viết trong Class nên việc gọi cũng đỡ cực hơn. Trong chương trình HookDll, các bạn bổ sung các đoạn code sau:
CODE
//Global Functions
void (*SendCmdRunToServer)(int, int);
void (*SendCmdWalkToServer)(int, int);
#define RUN_FUNC_ADD 0x004C41B7 // Send Client DoRun Function
#define WLK_FUNC_ADD 0x004C4204 // Send Client DoWalk Function
Và khởi tạo các địa chỉ hàm SendCmdRunToServer & hàm SendCmdWalkToServer bằng cách chèn đoạn code sau vào vị trí nào thích hợp (nhớ rằng chúng ta phải thực hiện việc này trong HookProc, tức là đang ở trong Process của Game. Việc gán trực tiếp bằng toán tử gán ( = ) sẽ ko thực hiện được vì VC++ sẽ báo lỗi nên ta dùng đoạn mã ASM.
CODE
__asm
{
mov SendCmdRunToServer, RUN_FUNC_ADD;
mov SendCmdWalkToServer, WLK_FUNC_ADD;
}
Đến đây chúng ta đã hoàn tất 90% công việc rồi, quay trở lại phần code của chương trình chính, sau khi ra đã gán các giá trị CmdKind, Param_X, Param_Y ta phải gửi một thông điệp WM_HOOK_WRITE nữa để báo HookProc chạy hàm SendCmdRunToServer hoặc SendCmdWalkToServer.
Đoạn mã đó như sau:
CODE
//SendMessage(m_hVLWindow, WM_HOOK_WRITE, 2, 0); // Send Walk Cmd
SendMessage(m_hVLWindow, WM_HOOK_WRITE, 3, 0); // Send Run Cmd
Còn ở HookProc, các bạn cải tiến một chút như dưới đây thì ta sẽ có được đoạn gửi lệnh RUN, WALK lên Server.
CODE
#define pCW ((CWPSTRUCT*)lParam)
LRESULT HookProc (int nCode, WPARAM wParam, LPARAM lParam)
{
int nAdd = pCW->wParam;
int nVal = pCW->lParam;
int *pX, *pY;
switch (nAdd)
{
case 2: // DO WALK
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdWalkToServer(*pX, *pY);
break;
case 3: // DO RUN
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdRunToServer(*pX, *pY);
break;
default: // ASSIGN OTHER VALUES
pX = (int*)nAdd;
*pX = nVal;
}
}
Kết thúc loạt bài về viết chương trình AutoPlay. Từ đây các bạn có thể cải tiến chương trình theo ý thích của mình, làm bao nhiêu chức năng tùy ý. Các đoạn code trên có nhiều chỗ tôi gõ trực tiếp lên đây (ko phải copy từ chương trình ra vì chỉ cần lấy phần cơ bản cho dễ hiểu) nên đôi khi gõ tắt hoặc gõ lỗi tí xíu, các bạn tự sửa lấy nhé.
Bài 1 – Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ
Bài 3 – Điều khiển nhân vật chạy
Ở đây không bàn đến việc dùng kỹ thuật đồ họa và chiếm chuột để điều khiển nhân vật. Các phần mềm hiện nay thường dùng chương trình chính Autoplay thực hiện việc đọc bộ nhớ Game lấy thông tin và dùng cơ chế Hook để ghi thông tin cần thay đổi vào bộ nhớ Game. Một điểm lưu ý là khi Hook sẽ chiếm tài nguyên, nên ta có thể dùng một mẹo nhỏ, sau khi Hook ta Unhook ngay lập tức và dùng LoadLibrary để nạp hook.dll vào bộ nhớ game, thay đổi địa chỉ WndProc để thực hiện nhiều ý đồ khác.. Ngoài ra, để chương trình có thêm nhiều chức năng, có thể ta sẽ phải gọi các hàm trong game_y.exe và khi đó ta phải mở rộng cái Hook.dll để có thể hook + inject code vào Game process (sẽ nói trong bài 3).
Bài 1 - Lấy các thông tin cơ bản về nhân vật và tự BUFF máu, mana
Tất cả các nhân vật (người chơi, quái, Npc...) được lưu thông tin trong mảng gồm 256 đối tượng, mỗi đối tượng kích thước 0x7E4C, địa chỉ lưu địa chỉ của mảng trên là 0x00D3A570. Đối tượng 0 để trống, đối tượng 1 là người chơi, còn lại là quái & Npc.
Khi chưa dùng cơ chế inject code nói trên ta có thể đọc bộ nhớ của Game bằng hàm ReadProcessMemory. Dưới đây là minh họa lớp đối tượng Npc và một số biến thành viên cần dùng (phần ... là phần dữ liệu chưa dùng tới, các bạn phải đặt một mảng BYTE với kích thước tương ứng vào đó để các địa chỉ OFFSET của các biến được chính xác)
CODE
#define NPC_BASE_ADD 0x00D3A570
#define NPC_DATA_SIZE 0x00007E4C
#define MAX_NPC 256
#define PLAYER_INDEX 1
class CNpc
{
...
DWORD m_NpcKind; //0x0020 1=mod, 1=player,...
...
DWORD m_Doing; //0x00EC 1=stand, 2=walk, 3=run...
...
int m_CurLife; //0x0B44
int m_CurLifeMax;
int m_CurLifeRep;
int m_CurMana;
int m_CurManaMax;
...
BOOL m_bRideHorse; //0x0D58
char Name[32]; //0x0D5C Ten nhan vat
int m_nSex;
...
BOOL m_FightMode; //0x0EAC
};
// Tim cua so va xac dinh Game Process Handle o day
...
CNpc Npc;
LPBYTE lpBaseAdd, lpCurAdd;
//Read start Address of Npc Array
ReadProcessMemory(m_hVLProcess, (LPBYTE)NPC_BASE_ADD, (LPVOID)&lpBaseAdd, 4, NULL);
//Start Address of PLAYER
lpCurAdd = lpBaseAdd + PLAYER_INDEX*NPC_DATA_SIZE;
//Read PLAYER ìnormation
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
...
Khi đọc được các thông số máu và mana rồi ta có thể dùng hàm PostMessage(hWnd, WM_KEYDOWN...) để gửi phím 1, 2, 3. Các bạn thấy ở trên có 1 biến m_FightMode dùng để xác định nhân vật đang ở chế độ đánh (ở bản đồ có quái) hoặc đang ở trong thành (không dùng chiêu được). Căn cứ vào đó ta có thể gửi phím 1, 2, 3 khi cần thiết (m_FightMode=TRUE). Nếu nhân vật về thành rồi khỏi bơm máu nữa (nhiều khi dính độc về cả phút mà cứ đứng trong thành bơm máu thì phí quá).
Linh tinh khác: Game VLTK có định nghĩa một số hàm macro có thể dùng ở dòng gõ CHAT, ví dụ:
“/Switch Horse”, “/Switch Sit”, “/SayPhrase 0”, , “/SayPhrase 1” các bạn thử mà xem, cũng thũ vị lắm. Các bạn mở file UIconfig.ini sẽ thấy nhiều hàm hơn. Dưới đây là đoạn code mà có thể thêm vào chương trình của mình dùng để chat hoặc chạy một hàm macro trong Game. Bạn có thể lập trình lên xuống ngựa hay ngồi xuống, đứng lên, ... mà ko phải dùng WM_KEYDOWN gửi phím tắt V, M (vì ở chế độ phím Mặc định, nhấn V sẽ ko ngồi đâu).
CODE
void PostChatMessage(LPCTSTR szChatMsg)
{
//Set focus to CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
//Clear CHAT edit control
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_DOWN, 0x00500001);
while (szChatMsg[0])
{
::PostMessage(m_hVLWin, WM_CHAR, LOBYTE(szChatMsg[0]), 0);
szChatMsg++;
}
//Set focus to Game Window
::PostMessage(m_hVLWin, WM_KEYDOWN, VK_RETURN, 0x001C0001);
}
Đến đây các bạn có thể lập được một chương trình tự buff mana, ngồi rao bán hàng rồi đấy. Ở bài 2 tôi sẽ nói về cách xác định tọa độ nhân vật (cũng giống như của các Npc khác) và đồ. Địa chỉ OFFSET tọa độ của nhân vật & Npc không phải là 0x10F8 như các bạn tưởng đâu, đó là địa chỉ đích Npc sẽ chạy tới, sẽ đánh tới... nếu nhân vật ngồi xuống, giá trị này sẽ bằng 0.
Bây giờ đến phần toạ độ của nhân vật. Bổ sung thêm các biến thành viên vào lớp CNpc, chúng ta có các thông tin về toạ độ và thông tin đối tượng cần đánh/ theo sau/ đồ cần nhặt:
CODE
class CNpc
{
public:
...
int m_NextAdd; //0x000C – xac dinh Npc ton tai hay ko
...
int m_ActiveSkill; //0x0114 – ky nang dang su dung
...
int m_MapX, m_MapY, m_MapZ; //0x0D0C – toa do Cell
int m_OffX, m_OffY; //0x0D18 – toa do Npc trong Cell (*1024)
...
int m_nPeopleIdx; //0x0DE0 Npc can tan cong, hoac theo sau
...
int m_nObjectIdx; //0x0DEC Obj can nhat
...
WORD m_RegX; //0x0EC4 – toa do Region
WORD m_RegY;
public:
void GetMapPos(int *nX, int *nY);
};
Toàn bàn đồ được chia thành nhiều vùng, có kích thước 512x1024, mỗi vùng lại chia thành nhiều cell, có kích thước là 32x32. Hai biến WORD RegX, RegY lưu giá trị vùng (Region), các biến MapX, MapY lưu giá trị của cell; OffX,OffY là toạ độ Npc trong ô đó (giá trị = 1024/pixel).
Toạ độ của nhân vật được tính như sau:
CODE
#define REGION_WIDTH 512
#define REGION_HEIGHT 1024
#define CELL_WIDTH 32
#define CELL_HEIGHT 32
void CNpc::GetMapPos(int *nX, int *nY)
{
*nX = m_RegX*REGION_WIDTH + m_MapX*CELL_WIDTH + (m_OffX>>10);
*nY = m_RegY*REGION_HEIGHT + m_MapY*CELL_HEIGHT + (m_OffY>>10);
}
Hàm sẽ trả về nX, nY giá trị toạ độ tuyệt đối của nhân vật/ Npc trên bản đồ, tính theo pixel (không phải toạ độ vẫn thường nói trong GAME đâu). Còn toạ độ trong Game (thể hiện trên bản đồ nhỏ) sẽ được tính bằng:
CODE
nOrgX = nX>>8; // dich phai 8 bit, chia cho 256
nOrgY = nY>>9; // dich phai 9 bit, chia cho 512
Đến đây các bạn có thể xác định được toạ độ của nhân vật cũng như của Npc bất kỳ trong mảng 256 Npc rồi, và do đó sẽ xác định được khoảng các giữa chúng. Vấn đề là khi quái bị chết, không phải tất cả dữ liệu trong đối tượng bị xoá, trừ khi có quái hoặc player khác được nạp vào vùng nhớ đó. Tôi có thấy 1 địa chỉ ở OFFSET 0x000C, tạm đặt tên biến là m_NextAdd vì thấy nó lưu các địa chỉ quanh quanh trong mảng 256 Npc, khi quái chết, hoặc ra khỏi vùng nhớ (đi ra xa), giá trị m_NextAdd được đặt về 0..
Bài 2 – Lấy thông tin về tọa độ nhân vật, tự tìm quái đánh, tự nhặt đồ
ây giờ đến phần toạ độ của nhân vật. Bổ sung thêm các biến thành viên vào lớp CNpc, chúng ta có các thông tin về toạ độ và thông tin đối tượng cần đánh/ theo sau/ đồ cần nhặt:
CODE
class CNpc
{
public:
...
int m_NextAdd; //0x000C – xac dinh Npc ton tai hay ko
...
int m_ActiveSkill; //0x0114 – ky nang dang su dung
...
int m_MapX, m_MapY, m_MapZ; //0x0D0C – toa do Cell
int m_OffX, m_OffY; //0x0D18 – toa do Npc trong Cell (*1024)
...
int m_nPeopleIdx; //0x0DE0 Npc can tan cong, hoac theo sau
...
int m_nObjectIdx; //0x0DEC Obj can nhat
...
WORD m_RegX; //0x0EC4 – toa do Region
WORD m_RegY;
public:
void GetMapPos(int *nX, int *nY);
};
Toàn bàn đồ được chia thành nhiều vùng, có kích thước 512x1024, mỗi vùng lại chia thành nhiều cell, có kích thước là 32x32. Hai biến WORD RegX, RegY lưu giá trị vùng (Region), các biến MapX, MapY lưu giá trị của cell; OffX,OffY là toạ độ Npc trong ô đó (giá trị = 1024/pixel).
Toạ độ của nhân vật được tính như sau:
CODE
#define REGION_WIDTH 512
#define REGION_HEIGHT 1024
#define CELL_WIDTH 32
#define CELL_HEIGHT 32
void CNpc::GetMapPos(int *nX, int *nY)
{
*nX = m_RegX*REGION_WIDTH + m_MapX*CELL_WIDTH + (m_OffX>>10);
*nY = m_RegY*REGION_HEIGHT + m_MapY*CELL_HEIGHT + (m_OffY>>10);
}
Hàm sẽ trả về nX, nY giá trị toạ độ tuyệt đối của nhân vật/ Npc trên bản đồ, tính theo pixel (không phải toạ độ vẫn thường nói trong GAME đâu). Còn toạ độ trong Game (thể hiện trên bản đồ nhỏ) sẽ được tính bằng:
CODE
nOrgX = nX>>8; // dich phai 8 bit, chia cho 256
nOrgY = nY>>9; // dich phai 9 bit, chia cho 512
Đến đây các bạn có thể xác định được toạ độ của nhân vật cũng như của Npc bất kỳ trong mảng 256 Npc rồi, và do đó sẽ xác định được khoảng các giữa chúng. Vấn đề là khi quái bị chết, không phải tất cả dữ liệu trong đối tượng bị xoá, trừ khi có quái hoặc player khác được nạp vào vùng nhớ đó. Tôi có thấy 1 địa chỉ ở OFFSET 0x000C, tạm đặt tên biến là m_NextAdd vì thấy nó lưu các địa chỉ quanh quanh trong mảng 256 Npc, khi quái chết, hoặc ra khỏi vùng nhớ (đi ra xa), giá trị m_NextAdd được đặt về 0.
Vậy thuật toán đơn giản để tìm quái để tấn công sẽ như sau:
CODE
CNpc Player; Npc;
int nX, nY;
int nMinIndex, nMinDistance=2000;
//Read PLAYER information
lpCurAdd = lpBaseAdd + NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Player, sizeof(CNpc), NULL);
for (int i=2; i<MAX_NPC; i++)
{
//Read NPC information
lpCurAdd = lpBaseAdd + i*NPC_DATA_SIZE;
ReadProcessMemory(m_hVLProcess, lpCurAdd, (LPVOID)&Npc, sizeof(CNpc), NULL);
if (!Npc.m_NextAdd) continue; // Npc does not exist
if (Npc.m_CurLife<=0 || Npc.m_NpcKind != 0) continue;
//Xac dinh toa do quai
Npc.GetMapPos(int &nX, &nY);
//Tinh khoang cach den nhan vat
int nDistance = ...
//Kiem tra xem khoang cach la nho nhat de tan cong truoc
if (nDistance<=nMinDistance)
{
nMinDistance = nDistance;
nMinIndex = i;
}
}
//Player.m_nPeopleIdx = nMinIndex; // Tan cong quai o gan nhat
SendMessage(WM_HOOK_WRITE,...); // Ghi gia tri vao bo nho Game thong qua Hook
Vòng for trên tôi viết đơn giản hoá để dễ hiểu, các bạn có thể viết thêm các chức năng kiểm tra lag (sau khi đánh một lúc mà quái ko mất máu thì đổi con khác). Có 1 lưu ý nhỏ ở đây là khi máu quái = 0 nó vẫn sống, < 0 nó mới chết nên trong việc kiểm tra đánh quái, các bạn đánh đến khi máu < 0 thì mới search tiếp con khác. Đó là việc ở trước vòng for này, còn trong vòng for ở trên vì sao tôi vẫn kiểm tra Npc.m_CurLife<=0 thì bỏ qua Npc, các bạn có thể tự tìm hiểu nhé.
Khi các bạn đã có thể lấy toạ độ quái thì nên chọn thuật toán đánh quái ở gần trước. Theo tôi mình ko nên chọn phương pháp đánh quái ít máu trước vì sẽ bị KS hết điểm kn và nhân vật phải chạy tới chạy lui tít xa, không tối ưu.
Tiếp theo ta sẽ nghiên cứu mảng lưu thông tin đồ rớt ra. Mảng này gồm 256 đối tượng, nằm bắt đầu ở địa chỉ tĩnh 0x00DC2750, kích thước mỗi đối tượng là 0x03C4. Ta chỉ cần lấy phần dữ liệu ở đầu đối tượng và định nghĩa thành lớp CObj của chúng ta như dưới đây.
CODE
class CObj //dung nham lan voi CObject cua VC++ day nhe
{
public:
int m_nID; // -1 la do ko ton tai, > 0 la ton tai
int m_nData1; // Unused data
int m_ObjKind; // 3=item, 4=money,..10=Property Box
int m_Index;
WORD m_RegX, m_RegY; // Toa do Region
int m_nData2[2]; // Unused data
int m_MapX, m_MapY; // 0x001C, toa do Cell
int m_OffX, m_OffY; // 0x0024, toa do Off trong Cell
int m_nData3[13]; // Unused data
char m_szName[32]; // 0x0060
int m_nData4; // Unused data
int m_nColor; // Mau cua do (0=trang, 3=xanh, ?=vang...)
int m_nData5[3]; // Unused data
int m_nMoneyNum; // Luong tien
public:
void GetMapPos(int *nX, int *nY);
};
Chặc, đến đây các bạn lại thấy quá dễ để tính tọa độ của đồ rồi đúng ko, cũng theo cách tính như ở CNpc thôi. Nhặt đồ thì dễ hơn đánh quái nhiều, tìm cái đồ nào ở gần thì nhặt. Khoảng cách có thể nhặt đô trong Game đặt là 64, nhưng nếu cần các bạn cứ tìm trong khoảng 200-300 gì đó, nhân vật chỉ cần nhích đi một chút là nhặt được đồ. Các bạn còn nhớ cái biến thành viên m_nObjectIdx trong lớp CNpc ko? Nó sẽ xác định địa chỉ cần gán cho đối tượng người chơi để nhặt đồ (cũng theo vị trí của đồ trong mảng 256 đối tượng kia)
Bổ sung: Lần đầu tiên khi nhân vật bắt đầu đánh, các bạn phải gán chiêu thức vào m_ActiveSkill ở OFFSET 0x0114 nhé (tham khảo thêm bài viết của TinhLaGi). Ngoài ra ở phần trên tôi có bỏ sót một biến thành viên tại địa chỉ Offset 0x0C58, đó là m_nSkillRadius - bán kính hiệu quả của chiêu thức. Trước khi gán lệnh đánh, bạn gán giá trị này một số nhỏ, ví dụ 75 chẳng hạn là bán kính quyền cơ bản, nhân vật sẽ tiếp sát rồi mới đánh quái. Nếu ko để chế độ đánh tiếp cận, sau khi đánh 1 chiêu có bán kính hiệu quả = 0 (ví dụ NLTD), các bạn lại phải gán lại số này đủ lớn, nếu ko nhân vật sẽ chạy lại sát quái mà vẫn ko với tay đánh được. Có thể gán = 75 hoặc thậm chí = 5000, game sẽ tự reset lại.
Thêm nữa, phần đọc tọa độ của nhân vật, Npc & Obj có một số bạn dùng địa chỉ offset 0x0D20, 0x1088.. gì đó. Thực ra các địa chỉ này chỉ là kết quả của các tính toán hoặc gán tham số tọa độ cho các thao tác của nhân vật nên đôi khi mình đọc ra sẽ thu được một kết quả không chính xác.
Bài viết tiếp theo tôi sẽ đề cập đến việc điều khiển nhân vật chạy đến một tọa độ cho trước
Bài 3 – Điều khiển nhân vật chạy
Nào hôm nay chúng ta sẽ nghiên cứu đến cái việc điều khiển quái chạy. Dưới đây là các biến thành viên bổ sung cho lớp CNpc:
CODE
class CNpc
{
public:
...
int CmdKind; //0x1088, 2=walk, 3=run
int Param_X;
int Param_Y;
};
Để cho nhân vật chạy hoặc đi bộ đến 1 điểm nào đó, các bạn gán các giá trị thích hợp vào các địa chỉ trên, bao gồm loại lệnh (chạy, đi bộ) và tọa độ X, Y.
SendMessage(WM_HOOK_WRITE, ...3);
SendMessage(WM_HOOK_WRITE, ...nX);
SendMessage(WM_HOOK_WRITE, ...nY);
Tuy nhiên, công việc ko chỉ dừng lại ở đó, nếu ko nhân vật sẽ tự giật lại vị trí cũ. Bước tiếp theo là chúng ta phải gửi một gói tin đến server, báo rằng nhân vật di chuyển đến tọa độ mới. Như các bạn biết, đôi khi mạng lag, ta điều khiển nhân vật chạy được một đoạn nó lại giật lại chỗ cũ là vì gói tin báo đã ko gửi đi được.
Để làm được việc đó ta sẽ viết code gọi hàm gửi tin đến server. Sau khi nghiên cứu, tôi đã tìm được địa chỉ các hàm gửi lệnh RUN và hàm gửi lệnh WALK trong game_y.exe. Cũng may là mấy hàm này ko viết trong Class nên việc gọi cũng đỡ cực hơn. Trong chương trình HookDll, các bạn bổ sung các đoạn code sau:
CODE
//Global Functions
void (*SendCmdRunToServer)(int, int);
void (*SendCmdWalkToServer)(int, int);
#define RUN_FUNC_ADD 0x004C41B7 // Send Client DoRun Function
#define WLK_FUNC_ADD 0x004C4204 // Send Client DoWalk Function
Và khởi tạo các địa chỉ hàm SendCmdRunToServer & hàm SendCmdWalkToServer bằng cách chèn đoạn code sau vào vị trí nào thích hợp (nhớ rằng chúng ta phải thực hiện việc này trong HookProc, tức là đang ở trong Process của Game. Việc gán trực tiếp bằng toán tử gán ( = ) sẽ ko thực hiện được vì VC++ sẽ báo lỗi nên ta dùng đoạn mã ASM.
CODE
__asm
{
mov SendCmdRunToServer, RUN_FUNC_ADD;
mov SendCmdWalkToServer, WLK_FUNC_ADD;
}
Đến đây chúng ta đã hoàn tất 90% công việc rồi, quay trở lại phần code của chương trình chính, sau khi ra đã gán các giá trị CmdKind, Param_X, Param_Y ta phải gửi một thông điệp WM_HOOK_WRITE nữa để báo HookProc chạy hàm SendCmdRunToServer hoặc SendCmdWalkToServer.
Đoạn mã đó như sau:
CODE
//SendMessage(m_hVLWindow, WM_HOOK_WRITE, 2, 0); // Send Walk Cmd
SendMessage(m_hVLWindow, WM_HOOK_WRITE, 3, 0); // Send Run Cmd
Còn ở HookProc, các bạn cải tiến một chút như dưới đây thì ta sẽ có được đoạn gửi lệnh RUN, WALK lên Server.
CODE
#define pCW ((CWPSTRUCT*)lParam)
LRESULT HookProc (int nCode, WPARAM wParam, LPARAM lParam)
{
int nAdd = pCW->wParam;
int nVal = pCW->lParam;
int *pX, *pY;
switch (nAdd)
{
case 2: // DO WALK
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdWalkToServer(*pX, *pY);
break;
case 3: // DO RUN
pX = (int*)(PlayerBaseAdd + 0x108C);
pY = (int*)(PlayerBaseAdd + 0x1090);
SendCmdRunToServer(*pX, *pY);
break;
default: // ASSIGN OTHER VALUES
pX = (int*)nAdd;
*pX = nVal;
}
}
Kết thúc loạt bài về viết chương trình AutoPlay. Từ đây các bạn có thể cải tiến chương trình theo ý thích của mình, làm bao nhiêu chức năng tùy ý. Các đoạn code trên có nhiều chỗ tôi gõ trực tiếp lên đây (ko phải copy từ chương trình ra vì chỉ cần lấy phần cơ bản cho dễ hiểu) nên đôi khi gõ tắt hoặc gõ lỗi tí xíu, các bạn tự sửa lấy nhé.