上一篇博文中,通过对压缩数据块的解压缩以及合并,得到了解压缩的字节数组。从现在开始,就要处理这个数据。
这个部分的数据主要包括两大类信息:一类是游戏开始前的信息,例如游戏地图,游戏玩家,队伍、种族情况,高级选项等等,这些信息都是在进入游戏之前已经确定的东西;另一类是游戏进行时的信息,这块包括玩家游戏过程中的操作、游戏中的聊天等。其中,游戏开始前的信息占解压缩后的数据的前一小部分,紧接着后面的一大部分保存着游戏进行时的信息。
本文介绍如何解析游戏开始前的信息。
游戏开始前的信息的结构:
注:在下面各部分结构解释中,灰色字体标注的信息不对其进行解析,就不再详细介绍,要想了解可以参考w3g_format.txt文档。
一、总体结构
1、4 字节:未知。
2、variable字节:主机玩家记录(详细查看【二、玩家记录】)。
3、variable字节:游戏名称,字符串,以0x00结束。
4、1字节:空字节,0x00。
5、variable字节:特殊编码的数据(包括游戏设置、地图、创建者),以0x00结束(详细查看【三、特殊编码的数据】)。
6、4字节:玩家数量。
7、4字节:游戏类型。
8、4字节:未知。
9、variable字节:加入游戏的玩家列表(详细查看:【四、加入游戏的玩家列表】以及【二、玩家记录】)。
10、variable字节:Slot列表(详细查看:【五、Slot列表】)。
二、玩家记录
1、1字节:玩家类型,0x00主机,0x16加入游戏的玩家(【四、加入游戏的玩家列表】)。
2、1字节:玩家ID。
3、variable字节:玩家名称,以0x00结束。
4、1字节:附加数据大小,0x01或0x08。
5、1或8字节:附加数据。
三、特殊编码的数据
这是一段特殊编码的数据,该部分需要解码后才能继续解析,解码的方式直接看下面的代码,这里不再介绍。
解码后:
1、4字节:游戏设置,这部分包含一些高级选项,如下图,不过这部分很少有人去改变,所以这里不再去解析了。
2、5字节:未知。
3、4字节:地图校验。
4、variable字节:地图路径,字符串,以0x00结束。
5、variable字节:创建者,字符串,以0x00结束。
四、加入游戏的玩家列表
如果有多个玩家加入游戏,每个玩家对应一个下面的结构。由于是加入游戏的玩家,所以每个玩家对应的数据都是0x16开头。当遍历到第一个字节不是0x16时玩家列表就结束了。注意,加入游戏的玩家列表中不包含电脑玩家,电脑玩家在【五、Slot列表】中。
1、variable字节:玩家记录(详细查看【二、玩家记录】)。
2、4字节:0x00000000。
五、Slot列表
一个Slot是指游戏开始前的界面的一个玩家位置。如下图,即是4个Slot。
1、1字节:固定0x19。
2、2字节:下面的数据的字节数。
3、1字节:Slot数量。
4、variable字节:Slot记录的列表,其中包含多个Slot记录,数量即上面一个字节的值(详细查看【六、Slot记录】)。
5、4字节:随机种子。
6、1字节:队伍、种族是否可选择。
7、1字节:地图中的位置数量。
六、Slot记录
每个Slot占9个字节:
1、1字节:对应的玩家ID,电脑玩家是0x00。
2、1字节:地图下载百分比(一般都是100)。
3、1字节:Slot状态,0x00空的,0x01关闭着的,0x02使用中的。
4、1字节:是否是电脑玩家,0x00非电脑玩家,0x01电脑玩家。
5、1字节:队伍,0~11分别表示队伍1到队伍12,12表示裁判或观看者。
6、1字节:颜色,0红1蓝2青3紫4黄5橘黄6绿7粉8灰9浅蓝10深绿11棕12裁判或观看者
7、1字节:种族,0x01/0x41人族,0x02/0x42兽族,0x04/0x44暗夜精灵,0x08/0x48不死族,0x20/0x60随机。
8、1字节:电脑难度,0x00简单的,0x01中等难度的,0x02令人发狂的。
9、1字节:障碍(也就是血量百分比),0x32,0x3C,0x46,0x50,0x5A,0x64之一,分别表示50%到100%。
Java解析:
创建一个UncompressedData类,用于处理解压缩后的数据。
UncompressedData.java
package com.xxg.w3gparser;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;
public class UncompressedData {
/**
* 解压缩的字节数组
*/
private byte[] uncompressedDataBytes;
/**
* 解析的字节位置
*/
private int offset;
/**
* 玩家列表
*/
private List<Player> playerList = new ArrayList<Player>();
/**
* 游戏名称
*/
private String gameName;
/**
* 地图路径
*/
private String map;
/**
* 游戏创建者名称
*/
private String createrName;
public UncompressedData(byte[] uncompressedDataBytes) throws UnsupportedEncodingException, W3GException {
this.uncompressedDataBytes = uncompressedDataBytes;
// 跳过前4个未知字节
offset += 4;
// 解析第一个玩家
analysisPlayerRecode();
// 游戏名称(UTF-8编码)
int begin = offset;
while(uncompressedDataBytes[offset] != 0) {
offset++;
}
gameName = new String(uncompressedDataBytes, begin, offset - begin, "UTF-8");
offset++;
// 跳过一个空字节
offset++;
// 解析一段特殊编码的字节串,其中包含游戏设置、地图和创建者
analysisEncodedBytes();
// 跳过PlayerCount、GameType、LanguageID
offset += 12;
// 解析玩家列表
while(uncompressedDataBytes[offset] == 0x16) {
analysisPlayerRecode();
// 跳过4个未知的字节0x00000000
offset += 4;
}
// GameStartRecord - RecordID、number of data bytes following
offset += 3;
// 解析每个Slot
byte slotCount = uncompressedDataBytes[offset];
offset++;
for(int i = 0; i < slotCount; i++) {
analysisSlotRecode(i);
}
// RandomSeed、RandomSeed、StartSpotCount
offset += 6;
}
/**
* 解析PlayerRecode
* @throws UnsupportedEncodingException
*/
private void analysisPlayerRecode() throws UnsupportedEncodingException {
Player player = new Player();
playerList.add(player);
// 是否是主机(0为主机)
byte isHostByte = uncompressedDataBytes[offset];
boolean isHost = isHostByte == 0;
player.setHost(isHost);
offset++;
// 玩家ID
byte playerId = uncompressedDataBytes[offset];
player.setPlayerId(playerId);
offset++;
// 玩家名称(UTF-8编码)
int begin = offset;
while(uncompressedDataBytes[offset] != 0) {
offset++;
}
String playerName = new String(uncompressedDataBytes, begin, offset - begin, "UTF-8");
player.setPlayerName(playerName);
offset++;
// 附加数据大小
int additionalDataSize = uncompressedDataBytes[offset];
offset++;
// 加上附加数据大小
offset += additionalDataSize;
}
/**
* 解析特殊编码的字节串
* @throws UnsupportedEncodingException
*/
private void analysisEncodedBytes() throws UnsupportedEncodingException {
int begin = offset;
while(uncompressedDataBytes[offset] != 0) {
offset++;
}
// 编码的数据和解码后的数据的长度
int encodeLength = offset - begin - 1;
int decodeLength = encodeLength - (encodeLength - 1) / 8 - 1;
// 编码的数据和解码后的数据
byte[] encodeData = new byte[encodeLength];
byte[] decodeData = new byte[decodeLength];
// 将编码字节串部分拷贝成一个单独的字节数组,便于解析
System.arraycopy(uncompressedDataBytes, begin, encodeData, 0, encodeLength);
// 解码(解码的代码来自于http://w3g.deepnode.de/files/w3g_format.txt文档4.3部分,由C语言代码翻译成Java)
byte mask = 0;
int decodePos = 0;
int encodePos = 0;
while (encodePos < encodeLength) {
if (encodePos % 8 == 0) {
mask = encodeData[encodePos];
} else {
if ((mask & (0x1 << (encodePos % 8))) == 0) {
decodeData[decodePos++] = (byte) (encodeData[encodePos] - 1);
} else {
decodeData[decodePos++] = encodeData[encodePos];
}
}
encodePos++;
}
// 直接跳过游戏设置,这部分不再解析了
int decodeOffset = 13;
int decodeBegin = decodeOffset;
// 地图路径
while(decodeData[decodeOffset] != 0) {
decodeOffset++;
}
map = new String(decodeData, decodeBegin, decodeOffset - decodeBegin, "UTF-8");
decodeOffset++;
// 主机(游戏创建者)玩家名称
decodeBegin = decodeOffset;
while(decodeData[decodeOffset] != 0) {
decodeOffset++;
}
createrName = new String(decodeData, decodeBegin, decodeOffset - decodeBegin, "UTF-8");
decodeOffset++;
offset++;
}
/**
* 解析每个Slot
*/
private void analysisSlotRecode(int slotNumber) {
// 玩家ID
byte playerId = uncompressedDataBytes[offset];
offset++;
// 跳过地图下载百分比
offset++;
// 状态 0空的 1关闭的 2使用的
byte slotStatus = uncompressedDataBytes[offset];
offset++;
// 是否是电脑
byte computerPlayFlag = uncompressedDataBytes[offset];
boolean isComputer = computerPlayFlag == 1;
offset++;
// 队伍
byte team = uncompressedDataBytes[offset];
offset++;
// 颜色
byte color = uncompressedDataBytes[offset];
offset++;
// 种族
byte race = uncompressedDataBytes[offset];
offset++;
// 电脑难度
byte aiStrength = uncompressedDataBytes[offset];
offset++;
// 障碍(血量百分比)
byte handicap = uncompressedDataBytes[offset];
offset++;
// 设置玩家列表
if(slotStatus == 2) {
Player player= null;
if(!isComputer) {
player = getPlayById(playerId);
} else {
player = new Player();
playerList.add(player);
}
player.setComputer(isComputer);
player.setAiStrength(aiStrength);
player.setColor(color);
player.setHandicap(handicap);
player.setRace(race);
player.setTeamNumber(team);
player.setSlotNumber(slotNumber);
}
}
/**
* 通过玩家ID获取Player对象
* @param playerId 玩家ID
* @return 对应的Player对象
*/
private Player getPlayById(byte playerId) {
Player p = null;
for(Player player : playerList) {
if(playerId == player.getPlayerId()) {
p = player;
break;
}
}
return p;
}
public List<Player> getPlayerList() {
return playerList;
}
public String getGameName() {
return gameName;
}
public String getMap() {
return map;
}
public String getCreaterName() {
return createrName;
}
}
Player类表示每个玩家的信息,包括电脑玩家。其中slotNumber表示玩家的Slot位置,从0开始,后面将会用于解析聊天信息。
Player.java
package com.xxg.w3gparser;
public class Player {
/**
* 是否是主机
*/
private boolean isHost;
/**
* 玩家ID
*/
private byte playerId;
/**
* 玩家的Slot位置
*/
private int slotNumber;
/**
* 玩家名称
*/
private String playerName;
/**
* 是否是电脑
*/
private boolean isComputer;
/**
* 0~11:队伍1~12
* 12:裁判或观看者
*/
private byte teamNumber;
/**
* 玩家颜色,0红1蓝2青3紫4黄5橘黄6绿7粉8灰9浅蓝10深绿11棕12裁判或观看者
*/
private byte color;
/**
* 种族:0x01/0x41人族,0x02/0x42兽族,0x04/0x44暗夜精灵,0x08/0x48不死族,0x20/0x60随机
*/
private byte race;
/**
* 电脑级别:0简单的,1中等难度的,2令人发狂的
*/
private byte aiStrength;
/**
* 障碍,也就血量百分比,取值有50,60,70,80,90,100
*/
private byte handicap;
public boolean isHost() {
return isHost;
}
public void setHost(boolean isHost) {
this.isHost = isHost;
}
public byte getPlayerId() {
return playerId;
}
public void setPlayerId(byte playerId) {
this.playerId = playerId;
}
public String getPlayerName() {
return playerName;
}
public void setPlayerName(String playerName) {
this.playerName = playerName;
}
public boolean isComputer() {
return isComputer;
}
public void setComputer(boolean isComputer) {
this.isComputer = isComputer;
}
public byte getTeamNumber() {
return teamNumber;
}
public void setTeamNumber(byte teamNumber) {
this.teamNumber = teamNumber;
}
public byte getColor() {
return color;
}
public void setColor(byte color) {
this.color = color;
}
public byte getRace() {
return race;
}
public void setRace(byte race) {
this.race = race;
}
public byte getAiStrength() {
return aiStrength;
}
public void setAiStrength(byte aiStrength) {
this.aiStrength = aiStrength;
}
public byte getHandicap() {
return handicap;
}
public void setHandicap(byte handicap) {
this.handicap = handicap;
}
public int getSlotNumber() {
return slotNumber;
}
public void setSlotNumber(int slotNumber) {
this.slotNumber = slotNumber;
}
}
在Replay.java中,加入UncompressedData解析。
Replay.java
package com.xxg.w3gparser;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.zip.DataFormatException;
public class Replay {
private Header header;
private UncompressedData uncompressedData;
public Replay(File w3gFile) throws IOException, W3GException, DataFormatException {
// 将文件转为字节数组,方便处理
byte[] fileBytes = fileToByteArray(w3gFile);
// 解析Header
header = new Header(fileBytes);
// 遍历解析每个压缩数据块,解压缩,合并
long compressedDataBlockCount = header.getCompressedDataBlockCount();
byte[] uncompressedDataBytes = new byte[0]; // 所有压缩数据块中数据解压合并到这个数组中
int offset = 68;
for(int i = 0; i < compressedDataBlockCount; i++) {
CompressedDataBlock compressedDataBlock = new CompressedDataBlock(fileBytes, offset);
// 数组合并
byte[] blockUncompressedData = compressedDataBlock.getUncompressedDataBytes();
byte[] temp = new byte[uncompressedDataBytes.length + blockUncompressedData.length];
System.arraycopy(uncompressedDataBytes, 0, temp, 0, uncompressedDataBytes.length);
System.arraycopy(blockUncompressedData, 0, temp, uncompressedDataBytes.length, blockUncompressedData.length);
uncompressedDataBytes = temp;
int blockCompressedDataSize = compressedDataBlock.getCompressedDataSize();
offset += 8 + blockCompressedDataSize;
}
// 处理解压缩后的字节数组
uncompressedData = new UncompressedData(uncompressedDataBytes);
}
/**
* 将文件转换成字节数组
* @param w3gFile 文件
* @return 字节数组
* @throws IOException
*/
private byte[] fileToByteArray(File w3gFile) throws IOException {
FileInputStream fileInputStream = new FileInputStream(w3gFile);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int n;
try {
while((n = fileInputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, n);
}
} finally {
fileInputStream.close();
}
return byteArrayOutputStream.toByteArray();
}
public Header getHeader() {
return header;
}
public UncompressedData getUncompressedData() {
return uncompressedData;
}
}
修改main方法,测试以上代码。
Test.java
package com.xxg.w3gparser;
import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.zip.DataFormatException;
public class Test {
public static void main(String[] args) throws IOException, W3GException, DataFormatException {
Replay replay = new Replay(new File("C:/Documents and Settings/Administrator/桌面/131230_[UD]962030958_VS_[ORC]flygogogo_AncientIsles_RN.w3g"));
Header header = replay.getHeader();
System.out.println("版本:1." + header.getVersionNumber() + "." + header.getBuildNumber());
long duration = header.getDuration();
System.out.println("时长:" + convertMillisecondToString(duration));
UncompressedData uncompressedData = replay.getUncompressedData();
System.out.println("游戏名称:" + uncompressedData.getGameName());
System.out.println("游戏创建者:" + uncompressedData.getCreaterName());
System.out.println("游戏地图:" + uncompressedData.getMap());
List<Player> list = uncompressedData.getPlayerList();
for(Player player : list) {
System.out.println("---玩家" + player.getPlayerId() + "---");
System.out.println("玩家名称:" + player.getPlayerName());
if(player.isHost()) {
System.out.println("是否主机:主机");
} else {
System.out.println("是否主机:否");
}
if(player.getTeamNumber() != 12) {
System.out.println("玩家队伍:" + (player.getTeamNumber() + 1));
switch(player.getRace()) {
case 0x01:
case 0x41:
System.out.println("玩家种族:人族");
break;
case 0x02:
case 0x42:
System.out.println("玩家种族:兽族");
break;
case 0x04:
case 0x44:
System.out.println("玩家种族:暗夜精灵");
break;
case 0x08:
case 0x48:
System.out.println("玩家种族:不死族");
break;
case 0x20:
case 0x60:
System.out.println("玩家种族:随机");
break;
}
switch(player.getColor()) {
case 0:
System.out.println("玩家颜色:红");
break;
case 1:
System.out.println("玩家颜色:蓝");
break;
case 2:
System.out.println("玩家颜色:青");
break;
case 3:
System.out.println("玩家颜色:紫");
break;
case 4:
System.out.println("玩家颜色:黄");
break;
case 5:
System.out.println("玩家颜色:橘");
break;
case 6:
System.out.println("玩家颜色:绿");
break;
case 7:
System.out.println("玩家颜色:粉");
break;
case 8:
System.out.println("玩家颜色:灰");
break;
case 9:
System.out.println("玩家颜色:浅蓝");
break;
case 10:
System.out.println("玩家颜色:深绿");
break;
case 11:
System.out.println("玩家颜色:棕");
break;
}
System.out.println("障碍(血量):" + player.getHandicap() + "%");
if(player.isComputer()) {
System.out.println("是否电脑玩家:电脑玩家");
switch (player.getAiStrength()) {
case 0:
System.out.println("电脑难度:简单的");
break;
case 1:
System.out.println("电脑难度:中等难度的");
break;
case 2:
System.out.println("电脑难度:令人发狂的");
break;
}
} else {
System.out.println("是否电脑玩家:否");
}
} else {
System.out.println("玩家队伍:裁判或观看者");
}
}
}
private static String convertMillisecondToString(long millisecond) {
long second = (millisecond / 1000) % 60;
long minite = (millisecond / 1000) / 60;
if (second < 10) {
return minite + ":0" + second;
} else {
return minite + ":" + second;
}
}
}
程序输出:
版本:1.26.6059
时长:15:39
游戏名称:当地局域网内的游戏 (96
游戏创建者:962030958
游戏地图:Maps\E-WCLMAP\(2)AncientIsles.w3x
---玩家1---
玩家名称:962030958
是否主机:主机
SlotNumber:0
玩家队伍:1
玩家种族:不死族
玩家颜色:黄
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:flygogogo
是否主机:否
SlotNumber:1
玩家队伍:2
玩家种族:兽族
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否
参考文档:http://w3g.deepnode.de/files/w3g_format.txt
作者:叉叉哥 转载请注明出处:http://blog.youkuaiyun.com/xiao__gui/article/details/18218003