1. 程式人生 > >Java解析魔獸爭霸3錄影W3G檔案(一):Header

Java解析魔獸爭霸3錄影W3G檔案(一):Header

魔獸爭霸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類中,按小位元組序讀取所有的Header資訊。Header的最後四個位元組是CRC32迴圈冗餘檢驗碼,Java中可以使用java.util.zip.CRC32類來計算,下面的程式碼中校驗了計算結果和Header中是否一致。有關CRC32的介紹可以檢視相關資料,這裡不再介紹。

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;
	}
	
}

Header中用到了LittleEndianTool是用來按小位元組序讀取資料的工具類。

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);
	}

}

這裡需要注意的是,Java中int型別4個位元組大小,但是由於是有符號的整數,補碼的最高位是符號位,所以對於Header中的4個位元組的無符號整數,必須要用long型別才足夠。2個位元組的無符號整數需要使用Java中的int而不能是short。

另外,Header中用到了W3GException異常。

W3GException.java

package com.xxg.w3gparser;

public class W3GException extends Exception {
	
	public W3GException(String message) {
		super(message);
	}
	
}

最後用main方法呼叫這些程式碼來測試。

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