魔兽争霸3是一款非常著名的即时战略游戏。相信很多人都听过sky、moon、grubby这些名字,还有塔魔infi、中国的鬼王ted、刚猛的fly、飘逸的th000等选手。遗憾的是WCG2013是魔兽争霸3的最后一届,我自己也去现场观看了魔兽的总决赛。此外,还有DOTA、真三、澄海3C等著名的地图。
魔兽争霸的录像大家都知道,是用来回放的,文件后缀名是.w3g,保存在魔兽争霸下的REPLAY目录下。现在很多软件可以分析魔兽争霸录像,直接可以查看录像的玩家、地图,以及玩家的APM等信息。
最近在YY对战平台打魔兽,经常能遇到Java程序员,说明Java程序员中有很多魔兽争霸3的玩家,这里将Java解析魔兽争霸3录像的方法贡献给同是WAR3玩家的小伙伴们。
魔兽争霸3录像文件由一个头部(Header)和多个压缩数据块(compressed data blocks)组成。本文主要内容是解析Header部分,压缩数据块部分的解析会在后续的博文中详细介绍。
Header结构:
Header部分包含了录像的最基本的信息,大小是固定的前68个字节,此后的全部是压缩数据块。对于1.06版本及之前的录像,Header部分大小是64字节,由于版本太古老这里就不考虑了。下面的代码中也不再支持老版本的录像。
Header中每个部分的意义:
1~28字节(28个字符):固定的字符串"Warcraft III recorded game\0x1A\0"。
29~32字节(4个字节):Header部分的总字节数,对于1.07版本及之后,是68(0x44),对于1.06版本及之前是64(0x40)。
33~36字节(4个字节):压缩数据块的压缩数据总字节数,即解压前。
37~40字节(4个字节):录像版本标识(0表示1.06版本及之前版本,1表示1.07版本及之后版本)。
41~44字节(4个字节):压缩数据块解压缩后的总字节数。
45~48字节(4个字节):压缩数据块的个数。
49~52字节(4个字节):一个字符串标识,"WAR3"表示非冰封王座,"W3XP"表示冰封王座。
53~56字节(4个字节):版本号(例如24即是1.24版本)。
57~58字节(2个字节):构建号(build number)。
59~60字节(2个字节):0x0000表示单人游戏,0x8000(十进制32768)表示多人游戏。
61~64字节(4个字节):录像时长(毫秒数),需要注意的是,这个时长不包括游戏中暂停的时长。
65~68字节(4个字节):Header部分CRC32校验码(包含这四个字节但是要都设为0)。
可以使用EditPlus的Hex Viewer方式打开w3g文件查看Header部分。
在这里可以发现一个问题,除了第一个字符串"Warcraft III recorded game\0x1A\0"以外,其他每个部分的字节顺序都是倒过来的。例如Header部分总字节数是0x44000000,"W3XP"字符串顺序是"PX3W"。这是因为这里使用的是小字节序(Little-Endian),也就是字节顺序和正常的顺序完全相反,所以在读取的时候应该将其倒过来读。
Java解析Header:
知道了Header部分的结构,下面就可以用Java语言来解析Header了。
首先定义一个Replay类,表示一场录像,构造函数传入录像文件File。为了方便,将文件转换成字节数组,再将字节数组传给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); } /** * 将文件转换成字节数组 * @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; } }
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字节:" + 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字节:" + headerSize); if (headerSize != 0x44) { throw new W3GException("不支持V1.06及以下版本的录像。"); } // 压缩文件大小 compressedDataSize = LittleEndianTool.getUnsignedInt32(fileBytes, 32); System.out.println("33-36字节:" + compressedDataSize); // header版本(版本小于或等于V1.06是0,版本大于或等于V1.07是1) headerVersion = LittleEndianTool.getUnsignedInt32(fileBytes, 36); System.out.println("37-40字节:" + headerVersion); if (headerVersion != 1) { throw new W3GException("不支持V1.06及以下版本的录像。"); } // 解压缩数据大小 uncompressedDataSize = LittleEndianTool.getUnsignedInt32(fileBytes, 40); System.out.println("41-44字节:" + uncompressedDataSize); // 压缩数据块数量 compressedDataBlockCount = LittleEndianTool.getUnsignedInt32(fileBytes, 44); System.out.println("45-48字节:" + compressedDataBlockCount); // WAR3:非冰封王座录像,W3XP冰封王座录像 versionIdentifier = LittleEndianTool.getString(fileBytes, 48, 4); System.out.println("49-52字节:" + versionIdentifier); // 版本号(例如1.24版本对应的值是24) versionNumber = LittleEndianTool.getUnsignedInt32(fileBytes, 52); System.out.println("53-56字节:" + versionNumber); // Build号 buildNumber = LittleEndianTool.getUnsignedInt16(fileBytes, 56); System.out.println("57-58字节:" + buildNumber); // 单人游戏(0x0000) 多人游戏(0x8000,对应十进制32768) flag = LittleEndianTool.getUnsignedInt16(fileBytes, 58); System.out.println("59-60字节:" + flag); // 录像时长(毫秒) duration = LittleEndianTool.getUnsignedInt32(fileBytes, 60); System.out.println("61-64字节:" + duration); // CRC32校验码 long crc32 = LittleEndianTool.getUnsignedInt32(fileBytes, 64); System.out.println("65-68字节:" + crc32); // 这里来校验CRC32,将最后四位也就是CRC32所在的四个字节设为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; } }
LittleEndianTool.java
package com.xxg.w3gparser; /** * Little-Endian(小字节序)工具类 * @author 叉叉哥([email protected]) */ public class LittleEndianTool { /** * 以Little-Endian(小字节序)方式读取字节数组中的一个16位(2个字节)无符号整数 * @param bytes 字节数组 * @param offset 开始字节的位置索引 * @return 16位(2个字节)无符号整数 */ 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(小字节序)方式读取字节数组中的一个32位(4个字节)无符号整数 * @param bytes 字节数组 * @param offset 开始字节的位置索引 * @return 32位(4个字节)无符号整数 */ 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(小字节序)方式读取字节数组中的字符串 * @param bytes 字节数组 * @param offset 开始字节的位置索引 * @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); } }
另外,Header中用到了W3GException异常。
W3GException.java
package com.xxg.w3gparser; public class W3GException extends Exception { public W3GException(String message) { super(message); } }
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); } } }
1-28字节:Warcraft III recorded game
29-32字节:68
33-36字节:125736
37-40字节:1
41-44字节:311296
45-48字节:38
49-52字节:W3XP
53-56字节:24
57-58字节:6059
59-60字节:32768
61-64字节:783600
65-68字节:1414752232
计算CRC32:1414752232
WAR3录像基本信息为:
版本:1.24.6059
时长:13:03
参考文档:http://w3g.deepnode.de/files/w3g_format.txt
作者:叉叉哥 转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/17882303