上一篇博文中,通过对压缩数据块的解压缩以及合并,得到了解压缩的字节数组。从现在开始,就要处理这个数据。
这个部分的数据主要包括两大类信息:一类是游戏开始前的信息,例如游戏地图,游戏玩家,队伍、种族情况,高级选项等等,这些信息都是在进入游戏之前已经确定的东西;另一类是游戏进行时的信息,这块包括玩家游戏过程中的操作、游戏中的聊天等。其中,游戏开始前的信息占解压缩后的数据的前一小部分,紧接着后面的一大部分保存着游戏进行时的信息。
本文介绍如何解析游戏开始前的信息。
游戏开始前的信息的结构:
注:在下面各部分结构解释中,灰色字体标注的信息不对其进行解析,就不再详细介绍,要想了解可以参考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.csdn.net/xiao__gui/article/details/18218003