在上一篇博文中,通過解析壓縮數據塊解壓縮后的數據的前一部分,可以獲取到游戲開始前的一些信息,緊接著游戲開始前的信息之后,就是游戲進行時的信息了,其中包括玩家游戲中的操作,例如造建筑,出兵,攻擊,移動等,還包括玩家游戲中的聊天信息,玩家退出游戲等。
游戲進行時的信息由很多個數據塊組成。這些數據塊有幾種類型,每個數據塊的第一個字節就是數據塊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;// 未知的BlockIdcase 0x1A:case 0x1B:case 0x1C: offset += 5;break;case 0x22:offset += 6;break;case 0x23:offset += 11;break;case 0x2F:offset += 9;break;// 無效的Blockdefault: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、LanguageIDoffset += 12;// 解析玩家列表while(uncompressedDataBytes[offset] == 0x16) {analysisPlayerRecode();// 跳過4個未知的字節0x00000000offset += 4;}// GameStartRecord - RecordID、number of data bytes followingoffset += 3;// 解析每個Slotbyte slotCount = uncompressedDataBytes[offset];offset++;for(int i = 0; i < slotCount; i++) {analysisSlotRecode(i);}// RandomSeed、RandomSeed、StartSpotCountoffset += 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
總結
以上是生活随笔為你收集整理的Java解析魔兽争霸3录像W3G文件(四):解析游戏进行时的信息的全部內容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。