在上一篇博文中,通过解析压缩数据块解压缩后的数据的前一部分,可以获取到游戏开始前的一些信息,紧接着游戏开始前的信息之后,就是游戏进行时的信息了,其中包括玩家游戏中的操作,例如造建筑,出兵,攻击,移动等,还包括玩家游戏中的聊天信息,玩家退出游戏等。
游戏进行时的信息由很多个数据块组成。这些数据块有几种类型,每个数据块的第一个字节就是数据块ID,用于标识数据块的类型。
下面列出各种数据块类型及其对应的数据块ID、数据块字节数和数据块结构:
1、0x17 - 玩家离开游戏的数据块
数据块ID:0x17
数据块字节数:14字节
结构:
1字节:数据块ID,0x17;
2~5字节:原因;
6字节:玩家ID;
7~10字节:结果;
11~14字节:未知。
2、0x20 - 玩家聊天信息的数据块
数据块ID:0x20
数据块字节数:n + 4字节
结构:
1字节:数据块ID,0x20;
2字节:玩家ID;
3~4字节:数据块剩余的数据的字节数n;
5字节:flag;
6~9字节:聊天模式,0x00:对所有玩家,0x01:对队友,0x02:对裁判或观看者,0x03或大于0x03:对指定玩家,玩家的slotNumber是该值减去3的结果,注意这里是slotNumber而不是玩家ID;
10~n+4字节:聊天内容,字符串,最后一个字节是0x00。
3、0x1E/0x1F – 游戏时间段(TimeSlot)数据块
数据块ID:0x1E或0x1F
数据块字节数:n+3字节
结构:
1字节:数据块ID,0x1E或0x1F;
2~3字节:数据块剩余的数据的字节数n,最小值可能是2;
4~5字节:时间段的时间长度(毫秒,值一般是100毫秒左右);
6~n+3字节:玩家在这个时间段内的操作信息,当n为2时这部分不存在。这部分内容在下面一篇博文中再进行解析。
以上三种类型的数据块是需要解析的数据块,下面还有几种类型的数据块就不用去解析:
4、0x1A/0x1B/0x1C,5字节;
5、0x22,6字节;
6、0x23,11字节;
7、0x2F,9字节。
其中,游戏时间段(TimeSlot)数据块是游戏时间进度的标识,每个时间段100毫秒左右,其中包含玩家在这段时间内的操作信息。而游戏时间段数据块中的毫秒数累加后,就是游戏进行到的时间。比如玩家的操作、聊天、离开游戏的时间,就可以用其对应数据块之前的所有TimeSlot数据块中的时间累加来计算。
Java解析游戏进行时的信息:
添加ReplayData.java用来解析游戏进行时的信息。
ReplayData.java
package com.xxg.w3gparser; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.List; public class ReplayData { /** * 解压缩的字节数组 */ private byte[] uncompressedDataBytes; /** * 解析的字节位置 */ private int offset; /** * 玩家列表 */ private List<Player> playerList; /** * 游戏进行时的时间(毫秒) */ private long time; /** * 聊天信息集合 */ private List<ChatMessage> chatList = new ArrayList<ChatMessage>(); public ReplayData(byte[] uncompressedDataBytes, int offset, List<Player> playerList) throws W3GException, UnsupportedEncodingException { this.uncompressedDataBytes = uncompressedDataBytes; this.offset = offset; this.playerList = playerList; analysis(); } /** * 解析 */ private void analysis() throws UnsupportedEncodingException, W3GException { byte blockId = 0; while ((blockId = uncompressedDataBytes[offset]) != 0) { switch (blockId) { // 聊天信息 case 0x20: analysisChatMessage(); break; // 时间段(一般是100毫秒左右一段) case 0x1E: case 0x1F: analysisTimeSlot(); break; // 玩家离开游戏 case 0x17: analysisLeaveGame(); break; // 未知的BlockId case 0x1A: case 0x1B: case 0x1C: offset += 5; break; case 0x22: offset += 6; break; case 0x23: offset += 11; break; case 0x2F: offset += 9; break; // 无效的Block default: throw new W3GException("无效Block,ID:" + blockId); } } } /** * 解析聊天信息 */ private void analysisChatMessage() throws UnsupportedEncodingException { ChatMessage chatMessage = new ChatMessage(); offset++; byte playerId = uncompressedDataBytes[offset]; chatMessage.setFrom(getPlayById(playerId)); offset++; int bytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; offset++; long mode = LittleEndianTool.getUnsignedInt32(uncompressedDataBytes, offset); if(mode >= 3) { int receiverPlayerId = (int) (mode - 3); chatMessage.setTo(getPlayBySlotNumber(receiverPlayerId)); } chatMessage.setMode(mode); offset += 4; String message = new String(uncompressedDataBytes, offset, bytes - 6, "UTF-8"); chatMessage.setMessage(message); offset += bytes - 5; chatMessage.setTime(time); chatList.add(chatMessage); } /** * 解析一个时间块 */ private void analysisTimeSlot() { offset++; int bytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); offset += 2; int timeIncrement = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset); time += timeIncrement; offset += 2; offset += bytes - 2; } /** * 玩家离开游戏Block解析 */ private void analysisLeaveGame() { offset += 5; // 玩家离开游戏就不再计算游戏时间 byte playerId = uncompressedDataBytes[offset]; Player player = getPlayById(playerId); player.setPlayTime(time); offset += 9; } /** * 通过玩家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; } /** * 通过玩家SlotNumber获取Player对象 * @param slotNumber 玩家SlotNumber * @return 对应的Player对象 */ private Player getPlayBySlotNumber(int slotNumber) { Player p = null; for(Player player : playerList) { if(slotNumber == player.getSlotNumber()) { p = player; break; } } return p; } public List<ChatMessage> getChatList() { return chatList; } }
ChatMessage.java是玩家游戏过程中的聊天信息对应的Java对象,通过解析玩家聊天信息的数据块获取。
ChatMessage.java
package com.xxg.w3gparser; public class ChatMessage { /** * 发送者 */ private Player from; /** * 发送方式 * 0:发送给所有玩家 * 1:发送给队友 * 2:发送给裁判或观看者 * 3+N:发送给指定玩家 */ private long mode; /** * 接收者(mode为3+N时有效) */ private Player to; /** * 消息发送时间 */ private long time; /** * 消息内容 */ private String message; public Player getFrom() { return from; } public void setFrom(Player from) { this.from = from; } public long getMode() { return mode; } public void setMode(long mode) { this.mode = mode; } public Player getTo() { return to; } public void setTo(Player to) { this.to = to; } public long getTime() { return time; } public void setTime(long time) { this.time = time; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
由于玩家可能在游戏过程中离开游戏,要想知道玩家的实际游戏时间,就要排除玩家离开游戏之后的时间,而不能直接使用录像的时长。如果要计算APM的话,就必须使用玩家的实际游戏时间来计算。所以在Player.java中加入playTime表示实际游戏时间,通过解析玩家离开游戏的数据块来设置。
Player.java
/** * 游戏时间 */ private long playTime; public long getPlayTime() { return playTime; } public void setPlayTime(long playTime) { this.playTime = playTime; }
在UncompressedData类中加入对游戏进行时的信息的解析。
UncompressedData.java
/** * 游戏进行时的信息 */ private ReplayData replayData; 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; // 游戏进行时的信息解析 replayData = new ReplayData(uncompressedDataBytes, offset, playerList); } public ReplayData getReplayData() { return replayData; }
修改Test.java类中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/桌面/131229_[ORC]Sickofpast_VS_[NE]_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()); System.out.println("游戏时长:" + convertMillisecondToString(player.getPlayTime())); 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("玩家队伍:裁判或观看者"); } } List<ChatMessage> chatList = uncompressedData.getReplayData().getChatList(); for(ChatMessage chatMessage : chatList) { String chatString = "[" + convertMillisecondToString(chatMessage.getTime()) + "]"; chatString += chatMessage.getFrom().getPlayerName() + " 对 "; switch ((int)chatMessage.getMode()) { case 0: chatString += "所有人"; break; case 1: chatString += "队伍"; break; case 2: chatString += "裁判或观看者"; break; default: chatString += chatMessage.getTo().getPlayerName(); } chatString += " 说:" + chatMessage.getMessage(); System.out.println(chatString); } } 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
时长:14:43
游戏名称:当地局域网内的游戏 (Si
游戏创建者:Sickofpast
游戏地图:Maps\WAR3\(2)AncientIsles-2.w3x
---玩家1---
玩家名称:Sickofpast
游戏时长:14:43
是否主机:主机
玩家队伍:1
玩家种族:兽族
玩家颜色:红
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:尼德霍格
游戏时长:14:42
是否主机:否
玩家队伍:2
玩家种族:暗夜精灵
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否
[0:10]尼德霍格 对 所有人 说:All rights reserved by Blizzard
[0:10]尼德霍格 对 所有人 说:w3g files released by www.Replays.Net.
[0:15]Sickofpast 对 所有人 说:yy上都是菜鸟啊
[0:28]尼德霍格 对 所有人 说:diyi orc!
[0:37]尼德霍格 对 所有人 说:就yy有人玩了
[0:39]Sickofpast 对 所有人 说:打ne没赢过
[0:40]尼德霍格 对 所有人 说:中国第一ORC
[14:33]尼德霍格 对 所有人 说:For more replays, plz visit www.Replays.Net
[14:42]尼德霍格 对 所有人 说:g
参考文档:http://w3g.deepnode.de/files/w3g_format.txt
作者:叉叉哥 转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/18350789