门口那家卖卤肉肉夹馍的小店真的关门了,记事起便一直喜欢去那里吃,如今每天路过还会习惯性的看一眼.
这几天在研究魔兽3的录像文件.想做一个比skreplayparser与w3gmaster更加适合DOTA,比DotaReplay更精巧的录像分析器,哼哼,抛去APM统计什么的,还要实现下DotAReplay的脱机游戏模拟甚至更多辅助dota战术分析的功能.希望是能否把程序大小控制在50K以内.
因为暴雪对w3g格式是不公开的,所以民间多为猜测与逆向得来内容,目前为止的录像工具的实现多源自于http://w3g.deepnode.de/files/w3g_format.txt这篇文章,很感谢地精研究院翻译的那个版本,虽然里面有很多错误.
鉴于那篇文章太长,就想总结出精华写个拿来直接能用的W3G精简教程.
今天是把文件头和录像头的处理结构与代码写完了,下面加色部分为现成代码,在VS2008+XPSP3上正常运行,一些功能用到了WIN32API,除了Zlib外不依赖第三方库,也没用到类,可以用在所有Win32编译器中.
一:.w3g文件头部分:
/////////////////////W3G FILEHEAD(只支持魔兽争霸1.07之后版本)
#define MAX_SIZE 1024
//FileHead
structW3G_FILEHEAD//FileHead Size:0x44
{
charmagic[28];//Warcraft III recorded game x1A
unsignedintHeadSize;//0x44 TFT
unsignedintFileSize;//文件大小
intHeadVersion;//0x00 RoC, 0x01 TFT
unsignedintImageSize;//整个解压后的数据大小(含文件头)
unsignedintNumSection;//压缩数据块数目
charGame[4];//‘WAR3’为魔兽争霸原始版本,’W3XP’为资料片“冰封王座”
shortPackVersion;//补丁版本
charGameVersion[4];//主程序版本
shortGameType;//0x0000单人游戏,0x0008战网游戏
unsignedintGameLength;//游戏长度,毫秒为单位
unsignedintCRC32;//crc效验
W3G_SECTIONHEADFirstSectionHead;//数据
};
上面的最后一段是压缩数据段头:
/////////////////////W3G HEAD(只支持魔兽争霸1.07之后版本)
//数据段Head
structW3G_SECTIONHEAD//压缩数据段Head
{
shortSectionSize;//块大小,不包含Head
shortexSectionSize;//解压后块大小,固定为0x2000
intCRC;//???
chardata[];//DATA
};
一个W3G录像会有很多压缩数据段,压缩数据段本身的大小是不固定的由SectionSize值指出,加上SectionSize找到下一个数据段,轮流用Zlib解压,Zlib可以在http://zlib.net/找到,但每一个压缩数据段解压后的大小固定为0x2000,将这些大小为8KB的数据块拼接到一起便完成解压.那么w3g源文件的大小便是:压缩数据段的个数*8+所有SectionSize的和+文件头0x44.
//解压,返回解压后new出的内存指针
char* W3GZlib(const void* const pmemory,UINT&FileSize)//CreateFileMapping的内存镜像指针,原文件大小
{
char*pResult=0;
UINTNumSection=0,thisSectionSize;
W3G_SECTIONHEAD*pos=&((W3G_FILEHEAD*)(pmemory))->FirstSectionHead;
//统计解压后内存的大小
while(1)
{
//检查出界
if ((UINT)pos>=(UINT)(pmemory)+FileSize)
{
NumSection--;
break;
}
//add
NumSection++;
//++
thisSectionSize=(int)(&(pos->data))+(pos->SectionSize);
pos=(W3G_SECTIONHEAD*)thisSectionSize;
}
//new
pResult=newchar[0x2000*NumSection];
if(!pResult)
return 0;
RtlZeroMemory(pResult,0x2000*NumSection);
//Zlib
pos=&((W3G_FILEHEAD*)(pmemory) )->FirstSectionHead;
char*pBufferpos=pResult;
while(1)
{
//检查出界
if ((UINT)(&pos->data)+(pos->exSectionSize)>=(UINT)(pmemory)+FileSize)
{
break;
}
//解压
thisSectionSize=pos->exSectionSize;//0x2000
uncompress((unsignedchar*)pBufferpos,//http://zlib.net/
(unsignedlong*)(&thisSectionSize),//0x2000
(const unsignedchar*)(&pos->data[0]),//Section
pos->SectionSize);//Section Size
//++
pBufferpos+=thisSectionSize;
thisSectionSize=(int)(pos->data)+(pos->SectionSize);
pos=(W3G_SECTIONHEAD*)thisSectionSize;
}
returnpResult;
}
二.解压后录像文件头部分
因为地图名,玩家名等字符串均是C风格的不定长字符串,所以解压完后的这个文件头大小是不固定的
//DATAHEAD (解压完毕的)
struct W3G_REPLAYHEAD //???Size???
{
unsigned intmagic0;//0x00000110
//第一个玩家信息
charFlag;//0x00主机,0x16玩家
charPlayerIDid;//PlayerID
charPlayerName[MAX_SIZE];//???Size??? 0结尾
charGameType0;//0x01=自定义游戏,0x80=Ladder, 排除Ladder
charZero0;//自定义游戏为0
//游戏名称
charGameName[MAX_SIZE];//???Size???
charZero1;//0x00
//需解码:游戏设置与地图名称 or HostName
W3G_REPLAY_CODEcode;//???Size???
//游戏类型
intGameMaxPlayer;//理论最大玩家数,dota ==0x0000000C
charGameType1;//0x01
charGameType2;//0x00 局域网自定义游戏
shortZero2;//0x0000
intlanguage;//语言,数值不确定,随版本更替多次改变
//最多12玩家,Head开头有一个,这里最多11个
W3G_REPLAY_PLAYERplayers[12];//根据实际玩家大小不同???Size???
//游戏记录
charmagic1;//0x19
shortdataIndex;
charNumPlayer;//实际玩家数目
W3G_REPLAY_PLAYERPLACE place[12]; //??Size??
intRand;//随机数种子
charHostType;//队伍种族选择类型: 0x00 自由选择(对战),0x01 队伍锁定, 0x03 - 队伍和种族不可选 ,0x04 -固定的随机种族
charstart;//起始点位置
};
这里面的大量0结尾字符串导致数据头大小不确定,还有2个根据玩家人数不确定大小的数组和一个需要解码后才能填充的W3G_REPLAY_CODE结构,要分开对待:
//每个玩家的信息
struct W3G_REPLAY_PLAYER//???Size??? 不支持Ladder对战
{
charFlag;//0x00主机,0x16玩家
charPlayerIndex;//PlayerID
charPlayerName[MAX_SIZE];//0结尾
short GameType0;//0x0001=自定义游戏,0x0080=Ladder,排除Ladder
intZero;//自定义游戏为0x00000000
};
//游戏位置的信息均是标志位
struct W3G_REPLAY_PLAYERPLACE
{
charid;//电脑=0x00;
charmap;//地图类型:0x64 自定义游戏, 0xff ladder对战
charPlaceEnable;//0x00=空,0x01关闭,0x02使用
charIsComputer;//0x00=玩家,0x01=电脑
charteam;//队伍0-11,12是观察者和裁判
charcolour;//颜色Index,red, blue, cyan, purple, yellow, orange, green,pink, gray, light blue, dark green, brown
charPlayat;//种族:0x01=人类,0x02=兽人,0x04=暗夜精灵,0x08=亡灵天灾,0x20=随机,0x40=种族可选/固定
charBotAI;//玩家=0x01,0x00简单,0x01普通,0x02疯狂
charlife;//障碍,0x32, 0x3C, 0x46, 0x50, 0x5A, 0x64=100%
};
可能是暴雪为了不想让地图名与主机名暴露在明文中,所以有这么一段数据加码了,其中W3G_REPLAY_CODE结构是需要解码后才能使用,加密解码方法很简单,直接举个例子,这个W3G文件的W3G_REPLAY_CODEcode加密前是这样:
0x0216006C 01 03 49 07 41 01 77 01 d1 79 01 1315 fd a5 4d ..I.A.w..y.....M
0x0216007C cb 61 71 73 5d 45 4f 57 19 4f 4d 4f 4145 5d 45 .aqs]EOW.OMOAE]E
0x0216008C 2b 6f 75 41 21 41 6d 6d 2b 73 75 61 7373 21 77 +ouA!Amm+suass!w
0x0216009C c9 37 2f 35 37 2f 77 33 b1 79 01 75 7561 6d 61 .7/57/w3.y.uuama
0x021600AC 15 75 69 6f 39 39 01 01 00 0c 00 00 0001 00 00 .uio99..........
数据是每8字节为一组,8字节中的第一字节是控制字节,后7字节是数据,控制字节的作用是以控制字节的2~8bit(1bit永远为1分别对应8字节组中的第2~8个字节来控制解码,如果控制字节那一bit不为1,则对应的字节就要-1,整个数据段以0结尾.
上面头2个8字节数据段是
01(控制字节bit=00000001) 03 49 07 41 01 77 01
d1(控制字节bit=11010001) 79 01 13 15 fd a5 4d
控制字节0x01与0xd1分别是为00000001与11010001,按规则解密后便是
02 48 06 40 00 76 00
78 00 12 15 fc a5 4d
将除去数据段解码的7位全部拼接之后便是还原后的数据,
//解码数据块,返回源被解码的字节数
int W3GCodetoChar(char* pbuffer,char* psource)
{
char*EncodedString=psource;
char*DecodedString=pbuffer;
char mask;
int pos=0;
int dpos=0;
while(EncodedString[pos] != 0)
{
if (pos%8 == 0) mask=EncodedString[pos];
else
{
if ((mask & (0x1 <<(pos%8))) == 0)
DecodedString[dpos++] = EncodedString[pos] - 1;
else
DecodedString[dpos++] = EncodedString[pos];
}
pos++;
}
returnpos;//pos大于dpos
}
还原后的数据里明文已经出来了
0x0012B350 02 48 06 40 00 76 00 78 00 12 15 fca5 4d 61 70 .H.@.v.x.....Map
0x0012B360 73 5c 44 4f 57 4e 4c 4f 41 44 5c 44 6f74 41 20 sDOWNLOADDotA
0x0012B370 41 6c 6c 73 74 61 72 73 20 76 36 2e 3536 2e 77 Allstars v6.56.w
0x0012B380 33 78 00 74 75 61 6c 61 74 69 6e 39 3800 00 cc 3x.tualatin98...
现在,解码后的数据可以填充W3G_REPLAY_CODE结构.
struct W3G_REPLAY_CODE
{
charspeed;//游戏速度: 0 = 慢, 1 = 正常, 2 = 快, 3 = 未使用
charmapview;//前4位标志位:0x1=隐藏地形,0x2=探索过,0x4=总是可见,0x8=默认
//后4位标志位:5,6位:0x0关闭或为裁判,0x10=未使用 ,0x20=默认的观察者,0x30=打开或为裁判
//7位:0x40=共同队伍
charLockTeam;//锁定队伍: |0x6 = 开
charshare;// | 0x1 完全单位共享,0x2随机英雄,0x4随机种族,0x40观察者为裁判
charzero0;
charzero1;
charzero2;
charzero3;
charzero4;
intCRC;//效验
charMapName[MAX_SIZE];
charHostPlayerName[MAX_SIZE];
};
三.解压后文件头Format代码
写个函数填充结构头,调用后可以自由存取W3G_REPLAYHEAD中的内容,其中还调用了3个函数,有一个就是前面解码的
////////////////////////////////////////////////////////////////////////////////////////
//复制字符串,0结尾,返回复制字节数,若第一个为0也复制
unsigned int LoadChar(char* buffer,char* source)
{
char*b=buffer;
char*s=source;
unsigned intpos=0;
while(1)
{
if (*s==0)
{
*b=*s;
++pos;
break;
}else{
*b=*s;
++b;
++s;
++pos;
}
}
returnpos;
}
//装载玩家位置,返回结构大小
int LoadPlayerSection(char* image,W3G_REPLAY_PLAYER*arryplayer)
{
char*pos=image;
for (inti=0;i<12;++i)
{
if (*pos !=0x19)
{
arryplayer[i].Flag=*pos;
pos++;
arryplayer[i].PlayerIndex=*pos;
pos++;
pos+=LoadChar(&arryplayer[i].PlayerName[0],pos);
arryplayer[i].GameType0=*(short* )pos;
pos+=sizeof(short);
arryplayer[i].Zero=*(int* )pos;
pos+=sizeof(int);
}
}
returnpos-image;
}
//填充解压后的W3G_REPLAYHEAD结构头,返回第一个录像内容数据结构指针
char* W3GFormatHead(W3G_REPLAYHEAD&replayhead,char* pImage)
{
char*ReadPos=pImage;
unsigned intnum;
RtlZeroMemory(&replayhead,sizeof(W3G_REPLAYHEAD));
chartemp[MAX_SIZE];
//固定6字节
memcpy(&replayhead,ReadPos,6);
ReadPos+=6;
//第一个字符串
num=LoadChar(replayhead.PlayerName,ReadPos);
if(num>MAX_SIZE)
return 0;
ReadPos+=num;
//0x01=自定义游戏,0x80=Ladder,排除Ladder
replayhead.GameType0=*ReadPos;
++ReadPos;
//自定义游戏为0
replayhead.Zero0=*ReadPos;
++ReadPos;
//游戏名称
num=LoadChar(replayhead.GameName,ReadPos);
if(num>MAX_SIZE)
return 0;
ReadPos+=num;
//zero0
replayhead.Zero0=*ReadPos;
++ReadPos;
//Code
num=W3GCodetoChar(temp,ReadPos); //解码,temp包括地图与主机名
if(num>MAX_SIZE)
return 0;
ReadPos+=(num+1);//0结尾
memcpy(&replayhead.code,temp,0x9);
replayhead.code.CRC=*(int *)&temp[0x9];
num=LoadChar(replayhead.code.MapName,&temp[0xd]);//MapName
LoadChar(replayhead.code.HostPlayerName,&temp[0xd+num]);//HostName
//游戏类型
replayhead.GameMaxPlayer=*(int*)ReadPos;
ReadPos+=sizeof(int);
replayhead.GameType1=*ReadPos;
ReadPos++;
replayhead.GameType2=*ReadPos;
ReadPos++;
replayhead.Zero2=*(short*)ReadPos;
ReadPos+=sizeof(short);
replayhead.language=*(int*)ReadPos;
ReadPos+=sizeof(int);
//玩家信息数组
ReadPos+=LoadPlayerSection(ReadPos,replayhead.players);
//玩家数据
replayhead.magic1=*ReadPos;
ReadPos++;
replayhead.dataIndex=*(short*)ReadPos;
ReadPos+=sizeof(short);
replayhead.NumPlayer=*ReadPos;
ReadPos++;
//填充位置坑位
for (inti=0;i<replayhead.NumPlayer;i++)
{
memcpy(&replayhead.place[i],ReadPos,sizeof(W3G_REPLAY_PLAYERPLACE));
ReadPos+=sizeof(W3G_REPLAY_PLAYERPLACE);
}
replayhead.Rand=*(int*)ReadPos;
ReadPos+=sizeof(int);
replayhead.HostType=*ReadPos;
ReadPos++;
replayhead.start=*ReadPos;
ReadPos++;
return ReadPos;
}
////////////////////////////////////////////////////////////////////////////////////////////////////
至此W3G_REPLAYHEAD结构填充完毕,可以自由存取其中内容,W3GFormatHead返回的就是游戏录像最重要的数据内容第一个字节.