梦醒暗黑廿年
向抗击疫情的英雄们致敬
宇春秋
卌年未有过的闭门养猪生活,无病呻吟回忆年少游戏时光。
从英雄无敌三到文明四,突然想起廿年前通宵在网吧干暗黑二单机的日子。那时候什么都不会,一点支配骷髅都不点,一个人带着一大群骷髅干普通虫子,整个网吧看着我半个小时磨死都瑞尔的轰动,那一瞬间的满足,到廿年后的今天都历历在目。重玩暗黑二的时候,正好某网开剧毒世界,带着廿年的回忆,懵懂的冲了进去,然后在G战队群里看大佬们普及基本知识,一点一点的从零开始学符文之语,BUG杀,隔河杀…。
肝了一个月之后感觉心潮澎湃,只是每次蛮子手动BO得太麻烦,本想找个能自动BO的地图,网上找了一圈竟然没有,于是捡起OD想自己做个补丁,没想到一补就补了半个月,而且补出了这个系列(暂定叫系列吧,虽然绝大部分数据都有了,但第一次写文章,还不知道能有几篇:D)。
本系列只做学习交流,请不要用于其它非法目地。
另感谢sting大神,第一步的基础研究就是从读d2hackmap开始的,由于网上找了很久都没找到免费的1.13c版本源代码,只能随便找了个对应1.09版本的代码研究,后来还找了个d2hackmap2.24也就是对应1.11b版本的代码研究,两个版本的代码和文件结构变化很大,分析的时候两个版本可能有点混,但最后出的结果是好。
第一篇:地图篇
开始真没想过从地图开始研究起,起初只是想BO得找到玩家,也就是遍历周围数据,结果人物数据还没找到,反而把地图数据找出来了。
一、遍历数据。
首先想到的是不开地图进游戏,小地图上各个怪是没有文字提示的,开地图就有BOSS、精英怪和玩家的名字提示,所以这事肯定是地图做的。
翻d2hackmap的代码,里面正好有DrawAutomapCellPatch这个函数,看函数名应该是做这事的。仔细研读了下,发现…看不懂,对这个游戏的数据一点研究没有,硬来撞代码,肯定头疼。但没办法,先简单的分析一下。
函数的原型是这样的:
void __stdcall DrawAutomapCellPatch(CellContext *pCellContext, DWORD xpos, DWORD ypos, RECT *cliprect, DWORD bright)
里面有xpos和ypos,坐标嘛,那pCellContext肯定就是地图元素的结构了。函数里面有个常量CELLNO_WAYPOINT,定义的是
#define CELLNO_WAYPOINT 307
找个能用地图加载,OD上去在d2hackmap模块中搜索常量0x 133(十进制307)。
在d2hackmap+ 0x15E60(不同的地图偏移不同,请对应自己的地图分析,以后涉及hackmap模块的地址不在做说明)这个函数中有使用到这个常量。
在这个函数中所有的数据都用到了堆栈,而且用到的数据和地址没有关系,果断CTRL+F9返回上一层看有没有收获。
返回到的上一层是D2Client.dll,原型如下:
6FB104D9 |. 8B4424 20 |mov eax, dword ptr [esp+0x20]
6FB104DD |. 51 |push ecx
6FB104DE |. 8D5424 2C |lea edx, dword ptr [esp+0x2C]
6FB104E2 |. 52 |push edx
6FB104E3 |. 55 |push ebp
6FB104E4 |. 50 |push eax
6FB104E5 |. 8D4C24 48 |lea ecx, dword ptr [esp+0x48]
6FB104E9 |. 51 |push ecx
6FB104EA |. E8 71594CA0 |call d2hackma.0FFD5E60>>>>>>d2hackmap HOOK
6FB104EF |. 8B4C24 18 |mov ecx, dword ptr [esp+0x18]
6FB104F3 |> 3B6E 10 |cmp ebp, dword ptr [esi+0x10] ; D2Client.6FAF4B50
6FB104F6 |. 7F 0F |jg short 6FB10507
6FB104F8 |. 8B49 10 |mov ecx, dword ptr [ecx+0x10]>>>>关键点
6FB104FB |. 85C9 |test ecx, ecx
6FB104FD |. 894C24 18 |mov dword ptr [esp+0x18], ecx
在6FB104F8有这样的一行代码:mov ecx, dword ptr [ecx+0x10],翻译成C语言就是:
Addr=*(ULONG *)(Addr+0x10)
看样子不是链表就是二叉树。
那么最初的ecx地址是哪儿来的的呢?回到D2Client的调用函数头6FB10250下断,结果很迷茫,怎么返回的还是这个函数呢?
这种情况只有一种可能:递归调用。如果真是递归调用那么数据结构很大可能就是二叉树了,毕竟二叉树的经典遍历就是递归嘛!
到D2Client模块头,CTRL+F搜索call 6FB10250,看是哪儿最初调用这个递归的。
第一次搜索到的还是在6FB10250函数中,CTRL+L继续搜索,找到原型如下:
6FB10E05 |> \A1 C4C1BC6F mov eax, dword ptr [0x6FBCC1C4]
6FB10E0A |. 8B48 08 mov ecx, dword ptr [eax+0x8]
6FB10E0D |. 8D5424 10 lea edx, dword ptr [esp+0x10]
6FB10E11 |. E8 3AF4FFFF call 6FB10250>>>>>>>>调用1
6FB10E16 |. E8 E55D4CA0 call d2hackma.0FFD6C00
6FB10E1B |? 90 nop
6FB10E1C |. 8B49 0C mov ecx, dword ptr [ecx+0xC]
6FB10E1F |. 8D5424 10 lea edx, dword ptr [esp+0x10]
6FB10E23 |. E8 28F4FFFF call 6FB10250>>>>>>>>调用2
6FB10E28 |. A1 C4C1BC6F mov eax, dword ptr [0x6FBCC1C4]
6FB10E2D |. 8B48 10 mov ecx, dword ptr [eax+0x10]
6FB10E30 |. 8D5424 10 lea edx, dword ptr [esp+0x10]
6FB10E34 |. E8 17F4FFFF call 6FB10250>>>>>>>>调用3
6FB10E39 |. A1 B0C1BC6F mov eax, dword ptr [0x6FBCC1B0]
6FB10E3E |. 8B1D C4C1BC6F mov ebx, dword ptr [0x6FBCC1C4]
6FB10E44 |. 48 dec eax
6FB10E45 |. 8B43 04 mov eax, dword ptr [ebx+0x4]
看样子遍历了三个地址,分别是[[0x6FBCC1C4]+0x08], [[0x6FBCC1C4]+0x0C], [[0x6FBCC1C4]+0x10]。那么基本上可以确定以下三点:
1、遍历的基址是0x6FBCC1C4,也就是D2Client+0x11C1C4。
2、需要遍历三个地址里面的数据,分别是基址的0x08,0x0C,0x10三个偏移里的值。
3、再次回到6FB10250,发现遍历时下一数据分别储存在0x0C和0x10地址,那么可以99%确定是数据二叉树储存。
二、坐标信息
基本数据有了,但最关键的坐标呢?
不急,回到D2Client的6FB10250的函数看代码,原型如下:
6FB102A6 |> \0FBF41 06 |movsx eax, word ptr [ecx+0x6]
6FB102AA |. 8B2D B016BA6F |mov ebp, dword ptr [0x6FBA16B0]
6FB102B0 |. 8D0480 |lea eax, dword ptr [eax+eax*4]
6FB102B3 |. D1E0 |shl eax, 1
6FB102B5 |. 99 |cdq
6FB102B6 |. F7FD |idiv ebp
6FB102B8 |. 8BF8 |mov edi, eax
6FB102BA |. 2B3D F8C1BC6F |sub edi, dword ptr [0x6FBCC1F8]
6FB102C0 |. 0FBF41 08 |movsx eax, word ptr [ecx+0x8]
6FB102C4 |. 8D0480 |lea eax, dword ptr [eax+eax*4]
6FB102C7 |. D1E0 |shl eax, 1
6FB102C9 |. 99 |cdq
6FB102CA |. F7FD |idiv ebp
看6FB102A6的movsx eax, word ptr [ecx+0x6] ,还记得吗?ecx是地图元素的地址,word ptr [ecx+0x6]应该是个short型的数据,对应的就是xpos,那么word ptr [ecx+0x8]对应的就是ypos了。然后还有一些shl,div计算就不管了,大概应该是xpos(ypos)*5<<1/ [0x6FBA16B0],直接套汇编吧。
没读过什么书,没专门学过编程,所以代码很挫,看个大概意思就好(英语不好,很多单词都是BAIDU查的:L)。代码如下:
void GetMapCell(ULONG Addr,std::vector<ULONG> &arrMapCell)
{
if (IsBadReadPtr((LPVOID)Addr,0x100) != 0) return ;
arrMapCell.push_back(Addr);
GetMapCell(*(ULONG *)(Addr+0x0c),arrMapCell);
GetMapCell(*(ULONG *)(Addr+0x10),arrMapCell);
return;
}
void GetD2DllBase()
{
uD2ClientAddr=(ULONG)::GetModuleHandleA("D2Client.dll");
uD2CommonAddr=(ULONG)::GetModuleHandleA("D2Common.dll");
uD2WinAddr=(ULONG)::GetModuleHandleA("D2Win.dll");
uD2Multi=(ULONG)::GetModuleHandleA("D2Multi.dll");
uD2Launch = (ULONG)::GetModuleHandleA("d2launch.dll");
......
}
三、画出地图
1、由于暗黑2的坐标有负数,所以找出最小坐标和最大坐标,方便画出图形。
void InitCurMapInfo()
{
std::vector<ULONG> arrMapCell;
ULONG MapBase=*(ULONG *)(uD2ClientAddr+0x11C1C4);
if (IsBadReadPtr((LPVOID)MapBase, 0x20) != 0)return;
GetMapCell(*(ULONG *)(MapBase+0x0C),arrMapCell);//实际上0x0C是墙的数据
int MiniMapCellLen=*(int *)(uD2ClientAddr+0xF16B0);
int MinX=65535,MinY=65535;
int MaxX=0,MaxY=0;
for(ULONG i=0;i<arrMapCell.size();i++)
{
ULONG Addr=arrMapCell[i];
if (IsBadReadPtr((LPVOID)Addr,0x100) == 0)
{
int x=0,y=0;
_asm
{
pushad
mov ecx,Addr
movsx eax, word ptr [ecx+6]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov x,edi
mov ecx,Addr
movsx eax, word ptr [ecx+8]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov y,edi
popad
}
if(x<MinX)MinX=x;
if(y<MinY)MinY=y;
if(x>MaxX)MaxX=x;
if(y>MaxY)MaxY=y;
}
}
siCurMapMinX=MinX;
siCurMapMinY=MinY;
siCurMapMaxX=MaxX;
siCurMapMaxY=MaxY;
}
2、保存地图
void GetMapInfo()
{
std::vector<ULONG> arrMapCell;
ULONG MapBase=*(ULONG *)(uD2ClientAddr+0x11C1C4);
int MiniMapCellLen=*(int *)(uD2ClientAddr+0xF16B0);
InitCurMapInfo();
int MinX=siCurMapMinX,MaxX=siCurMapMaxX,MinY=siCurMapMinY,MaxY=siCurMapMaxY;
CImage image;
image.Create(MaxX-MinX,MaxY-MinY,24);//Create的背景是黑色的
for (int y=0; y<MaxY-MinY; y++)
{
BYTE *p=(BYTE *)image.GetPixelAddress(0,y);//黑色背景不好看,改成白色
memset(p,255,(MaxX-MinX)*3);
}
HDC hdc=image.GetDC();
SetBkMode(hdc,TRANSPARENT);
CFont font;
font.CreateFontA(20,0,0,0,200,FALSE,FALSE,0,ANSI_CHARSET,OUT_DEFAULT_PRECIS,CLIP_DEFAULT_PRECIS,DEFAULT_QUALITY,DEFAULT_PITCH|FF_SWISS,"Arial");
::SelectObject(hdc,font.GetSafeHandle());
GetMapCell(*(ULONG *)(MapBase+0x0C),arrMapCell);//0x0C是墙的数据
for(ULONG i=0;i<arrMapCell.size();i++)
{
ULONG Addr=arrMapCell[i];
if (IsBadReadPtr((LPVOID)Addr,0x100) == 0)
{
int x=0,y=0;
_asm
{
pushad
mov ecx,Addr
movsx eax, word ptr [ecx+6]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov x,edi
mov ecx,Addr
movsx eax, word ptr [ecx+8]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov y,edi
popad
}
x=x-MinX;
y=y-MinY;
image.SetPixel(x,y,RGB(255,0,0));
}
}
arrMapCell.clear();
GetMapCell(*(ULONG *)(MapBase+0x08),arrMapCell);//是地图附加元素,如水面
for(ULONG i=0;i<arrMapCell.size();i++)
{
ULONG Addr=arrMapCell[i];
if (IsBadReadPtr((LPVOID)Addr,0x100) == 0)
{
int x=0,y=0;
_asm
{
pushad
mov ecx,Addr
movsx eax, word ptr [ecx+6]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov x,edi
mov ecx,Addr
movsx eax, word ptr [ecx+8]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov y,edi
popad
}
x=x-MinX;
y=y-MinY;
if(*(WORD *)(Addr+4)>=4&&*(WORD *)(Addr+4)<=8)//0x08是地图元素,4-8是河水走不过去,需要画在地图上,其它的都画上去太乱不好看
{
image.SetPixel(x,y,RGB(0,0,255));
}
}
}
arrMapCell.clear();
GetMapCell(*(ULONG *)(MapBase+0x10),arrMapCell);//不知道是什么,没注意
for(ULONG i=0;i<arrMapCell.size();i++)
{
ULONG Addr=arrMapCell[i];
if (IsBadReadPtr((LPVOID)Addr,0x100) == 0)
{
int x=0,y=0;
_asm
{
pushad
mov ecx,Addr
movsx eax, word ptr [ecx+6]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov x,edi
mov ecx,Addr
movsx eax, word ptr [ecx+8]
mov ebx, MiniMapCellLen
lea eax, dword ptr [eax+eax*4]
shl eax, 1
cdq
idiv ebx
mov edi, eax
mov y,edi
popad
}
x=x-MinX;
y=y-MinY;
image.SetPixel(x,y,RGB(0,255,0));
}
}
image.ReleaseDC();
image.Save("C:\\map.bmp");
}
3、画完之后发现大概能看出地图的样子了,但每个点之间的差距有点大,不能成一个封闭的地图图形,我的做法是把每个地图坐标点除以一个值,走廊窄的地图/2,大地图/4。然后判断斜角点是否有数据,如果有数据就把周围点补齐,地图就封闭起来了。望高手指点怎么处理更好!结果如下:
本人原创,转载请注明。
第一篇地图篇完。(第二篇NPC篇待续)