魔獸爭霸3是一款非常著名的即時戰(zhàn)略游戲。相信很多人都聽過sky、moon、grubby這些名字,還有塔魔infi、中國的鬼王ted、剛猛的fly、飄逸的th000等選手。遺憾的是WCG2013是魔獸爭霸3的最后一屆,我自己也去現(xiàn)場觀看了魔獸的總決賽。此外,還有DOTA、真三、澄海3C等著名的地圖。
魔獸爭霸的錄像大家都知道,是用來回放的,文件后綴名是.w3g,保存在魔獸爭霸下的REPLAY目錄下。現(xiàn)在很多軟件可以分析魔獸爭霸錄像,直接可以查看錄像的玩家、地圖,以及玩家的APM等信息。
最近在YY對戰(zhàn)平臺打魔獸,經(jīng)常能遇到Java程序員,說明Java程序員中有很多魔獸爭霸3的玩家,這里將Java解析魔獸爭霸3錄像的方法貢獻給同是WAR3玩家的小伙伴們。
魔獸爭霸3錄像文件由一個頭部(Header) 和多個壓縮數(shù)據(jù)塊(compressed data blocks) 組成。本文主要內(nèi)容是解析Header部分,壓縮數(shù)據(jù)塊部分的解析會在后續(xù)的博文中詳細(xì)介紹。
Header結(jié)構(gòu):
Header部分包含了錄像的最基本的信息,大小是固定的前68個字節(jié),此后的全部是壓縮數(shù)據(jù)塊。對于1.06版本及之前的錄像,Header部分大小是64字節(jié),由于版本太古老這里就不考慮了。下面的代碼中也不再支持老版本的錄像。
Header中每個部分的意義:
1~28字節(jié)(28個字符):固定的字符串"Warcraft III recorded game\0x1A\0"。 29~32字節(jié)(4個字節(jié)):Header部分的總字節(jié)數(shù),對于1.07版本及之后,是68(0x44),對于1.06版本及之前是64(0x40)。 33~36字節(jié)(4個字節(jié)):壓縮數(shù)據(jù)塊的壓縮數(shù)據(jù)總字節(jié)數(shù),即解壓前。 37~40字節(jié)(4個字節(jié)):錄像版本標(biāo)識(0表示1.06版本及之前版本,1表示1.07版本及之后版本)。 41~44字節(jié)(4個字節(jié)):壓縮數(shù)據(jù)塊解壓縮后的總字節(jié)數(shù)。 45~48字節(jié)(4個字節(jié)):壓縮數(shù)據(jù)塊的個數(shù)。 49~52字節(jié)(4個字節(jié)):一個字符串標(biāo)識,"WAR3"表示非冰封王座,"W3XP"表示冰封王座。 53~56字節(jié)(4個字節(jié)):版本號(例如24即是1.24版本)。 57~58字節(jié)(2個字節(jié)):構(gòu)建號(build number)。 59~60字節(jié)(2個字節(jié)):0x0000表示單人游戲,0x8000(十進制32768)表示多人游戲。 61~64字節(jié)(4個字節(jié)):錄像時長(毫秒數(shù)),需要注意的是,這個時長不包括游戲中暫停的時長。 65~68字節(jié)(4個字節(jié)):Header部分CRC32校驗碼(包含這四個字節(jié)但是要都設(shè)為0)。
可以使用EditPlus的Hex Viewer方式打開w3g文件查看Header部分。
在這里可以發(fā)現(xiàn)一個問題,除了第一個字符串"Warcraft III recorded game\0x1A\0"以外,其他每個部分的字節(jié)順序都是倒過來的。例如Header部分總字節(jié)數(shù)是0x44000000,"W3XP"字符串順序是"PX3W"。這是因為這里使用的是小字節(jié)序(Little-Endian),也就是字節(jié)順序和正常的順序完全相反,所以在讀取的時候應(yīng)該將其倒過來讀。
Java解析Header:
知道了Header部分的結(jié)構(gòu),下面就可以用Java語言來解析Header了。
首先定義一個Replay類,表示一場錄像,構(gòu)造函數(shù)傳入錄像文件File。為了方便,將文件轉(zhuǎn)換成字節(jié)數(shù)組,再將字節(jié)數(shù)組傳給Header類進行處理。
Replay.java
package com.xxg.w3gparser;import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;public class Replay {private Header header;public Replay(File w3gFile) throws IOException, W3GException {byte[] fileBytes = fileToByteArray(w3gFile);header = new Header(fileBytes);}/*** 將文件轉(zhuǎn)換成字節(jié)數(shù)組* @param w3gFile 文件* @return 字節(jié)數(shù)組* @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;}}
在Header類中,按小字節(jié)序讀取所有的Header信息。Header的最后四個字節(jié)是CRC32循環(huán)冗余檢驗碼,Java中可以使用java.util.zip.CRC32類來計算,下面的代碼中校驗了計算結(jié)果和Header中是否一致。有關(guān)CRC32的介紹可以查看相關(guān)資料,這里不再介紹。
Header.java
package com.xxg.w3gparser;import java.util.zip.CRC32;public class Header {public static final String BEGIN_TITLE = "Warcraft III recorded game\u001A\0";private long headerSize;private long compressedDataSize;private long headerVersion;private long uncompressedDataSize;private long compressedDataBlockCount;private String versionIdentifier;private long versionNumber;private int buildNumber;private int flag;private long duration;public Header(byte[] fileBytes) throws W3GException {// 讀取開頭的字符串"Warcraft III recorded game\u001A\0"String beginTitle = new String(fileBytes, 0, 28);System.out.println("1-28字節(jié):" + beginTitle);if (!BEGIN_TITLE.equals(beginTitle)) {throw new W3GException("錄像格式不正確。");}// header部分總大小(版本小于或等于V1.06是0x40(64),版本大于或等于V1.07是0x44(68))headerSize = LittleEndianTool.getUnsignedInt32(fileBytes, 28);System.out.println("29-32字節(jié):" + headerSize);if (headerSize != 0x44) {throw new W3GException("不支持V1.06及以下版本的錄像。");}// 壓縮文件大小compressedDataSize = LittleEndianTool.getUnsignedInt32(fileBytes, 32);System.out.println("33-36字節(jié):" + compressedDataSize);// header版本(版本小于或等于V1.06是0,版本大于或等于V1.07是1)headerVersion = LittleEndianTool.getUnsignedInt32(fileBytes, 36);System.out.println("37-40字節(jié):" + headerVersion);if (headerVersion != 1) {throw new W3GException("不支持V1.06及以下版本的錄像。");}// 解壓縮數(shù)據(jù)大小uncompressedDataSize = LittleEndianTool.getUnsignedInt32(fileBytes, 40);System.out.println("41-44字節(jié):" + uncompressedDataSize);// 壓縮數(shù)據(jù)塊數(shù)量compressedDataBlockCount = LittleEndianTool.getUnsignedInt32(fileBytes, 44);System.out.println("45-48字節(jié):" + compressedDataBlockCount);// WAR3:非冰封王座錄像,W3XP冰封王座錄像versionIdentifier = LittleEndianTool.getString(fileBytes, 48, 4);System.out.println("49-52字節(jié):" + versionIdentifier);// 版本號(例如1.24版本對應(yīng)的值是24)versionNumber = LittleEndianTool.getUnsignedInt32(fileBytes, 52);System.out.println("53-56字節(jié):" + versionNumber);// Build號buildNumber = LittleEndianTool.getUnsignedInt16(fileBytes, 56);System.out.println("57-58字節(jié):" + buildNumber);// 單人游戲(0x0000) 多人游戲(0x8000,對應(yīng)十進制32768)flag = LittleEndianTool.getUnsignedInt16(fileBytes, 58);System.out.println("59-60字節(jié):" + flag);// 錄像時長(毫秒)duration = LittleEndianTool.getUnsignedInt32(fileBytes, 60);System.out.println("61-64字節(jié):" + duration);// CRC32校驗碼long crc32 = LittleEndianTool.getUnsignedInt32(fileBytes, 64);System.out.println("65-68字節(jié):" + crc32);// 這里來校驗CRC32,將最后四位也就是CRC32所在的四個字節(jié)設(shè)為0后計算CRC32的值CRC32 crc32Tool = new CRC32();crc32Tool.update(fileBytes, 0, 64);crc32Tool.update(0);crc32Tool.update(0);crc32Tool.update(0);crc32Tool.update(0);System.out.println("計算CRC32:" + crc32Tool.getValue());// 判斷Header中后四位讀取的CRC32的值和計算得到的值比較,看是否一致if (crc32 != crc32Tool.getValue()) {throw new W3GException("Header部分CRC32校驗不通過。");}}public long getHeaderSize() {return headerSize;}public long getCompressedDataSize() {return compressedDataSize;}public long getHeaderVersion() {return headerVersion;}public long getUncompressedDataSize() {return uncompressedDataSize;}public long getCompressedDataBlockCount() {return compressedDataBlockCount;}public String getVersionIdentifier() {return versionIdentifier;}public long getVersionNumber() {return versionNumber;}public int getBuildNumber() {return buildNumber;}public int getFlag() {return flag;}public long getDuration() {return duration;}}
Header中用到了LittleEndianTool是用來按小字節(jié)序讀取數(shù)據(jù)的工具類。
LittleEndianTool.java
package com.xxg.w3gparser;/*** Little-Endian(小字節(jié)序)工具類* @author 叉叉哥(806223819@qq.com)*/
public class LittleEndianTool {/*** 以Little-Endian(小字節(jié)序)方式讀取字節(jié)數(shù)組中的一個16位(2個字節(jié))無符號整數(shù)* @param bytes 字節(jié)數(shù)組* @param offset 開始字節(jié)的位置索引* @return 16位(2個字節(jié))無符號整數(shù)*/public static int getUnsignedInt16(byte[] bytes, int offset) {int b0 = bytes[offset] & 0xFF;int b1 = bytes[offset + 1] & 0xFF;return b0 + (b1 << 8);}/*** 以Little-Endian(小字節(jié)序)方式讀取字節(jié)數(shù)組中的一個32位(4個字節(jié))無符號整數(shù)* @param bytes 字節(jié)數(shù)組* @param offset 開始字節(jié)的位置索引* @return 32位(4個字節(jié))無符號整數(shù)*/public static long getUnsignedInt32(byte[] bytes, int offset) {long b0 = bytes[offset] & 0xFFl;long b1 = bytes[offset + 1] & 0xFFl;long b2 = bytes[offset + 2] & 0xFFl;long b3 = bytes[offset + 3] & 0xFFl;return b0 + (b1 << 8) + (b2 << 16) + (b3 << 24);}/*** 以Little-Endian(小字節(jié)序)方式讀取字節(jié)數(shù)組中的字符串* @param bytes 字節(jié)數(shù)組* @param offset 開始字節(jié)的位置索引* @param length 需要讀取的長度* @return 讀取的字符串*/public static String getString(byte[] bytes, int offset, int length) {byte[] temp = new byte[length];for(int i = 0; i < length; i++) {temp[i] = bytes[offset + length - i - 1];}return new String(temp);}}
這里需要注意的是,Java中int類型4個字節(jié)大小,但是由于是有符號的整數(shù),補碼的最高位是符號位,所以對于Header中的4個字節(jié)的無符號整數(shù),必須要用long類型才足夠。2個字節(jié)的無符號整數(shù)需要使用Java中的int而不能是short。
另外,Header中用到了W3GException異常。
W3GException.java
package com.xxg.w3gparser;public class W3GException extends Exception {public W3GException(String message) {super(message);}}
最后用main方法調(diào)用這些代碼來測試。
Test.java
package com.xxg.w3gparser;import java.io.File;
import java.io.IOException;public class Test {public static void main(String[] args) throws IOException, W3GException {Replay replay = new Replay(new File("E:/魔獸爭霸3冰封王座/REPLAY/100729_[NE]EHOME.ReMinD_VS_[ORC]WemadeFOX_Lyn_EchoIsles_RN.w3g"));Header header = replay.getHeader();System.out.println("WAR3錄像基本信息為:");System.out.println("版本:1." + header.getVersionNumber() + "." + header.getBuildNumber());long duration = header.getDuration();long second = (duration / 1000) % 60;long minite = (duration / 1000) / 60;if (second < 10) {System.out.println("時長:" + minite + ":0" + second);} else {System.out.println("時長:" + minite + ":" + second);}}}
輸出結(jié)果:
1-28字節(jié):Warcraft III recorded game ? 29-32字節(jié):68 33-36字節(jié):125736 37-40字節(jié):1 41-44字節(jié):311296 45-48字節(jié):38 49-52字節(jié):W3XP 53-56字節(jié):24 57-58字節(jié):6059 59-60字節(jié):32768 61-64字節(jié):783600 65-68字節(jié):1414752232 計算CRC32:1414752232 WAR3錄像基本信息為: 版本:1.24.6059 時長:13:03
參考文檔:http://w3g.deepnode.de/files/w3g_format.txt
作者:叉叉哥 ? 轉(zhuǎn)載請注明出處:http://blog.csdn.net/xiao__gui/article/details/17882303
總結(jié)
以上是生活随笔 為你收集整理的Java解析魔兽争霸3录像W3G文件(一):Header 的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
如果覺得生活随笔 網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔 推薦給好友。