Java解析魔兽争霸3录像W3G文件(三):解析游戏开始前的信息

上一篇博文中,通过对压缩数据块的解压缩以及合并,得到了解压缩的字节数组。从现在开始,就要处理这个数据。

这个部分的数据主要包括两大类信息:一类是游戏开始前的信息,例如游戏地图,游戏玩家,队伍、种族情况,高级选项等等,这些信息都是在进入游戏之前已经确定的东西;另一类是游戏进行时的信息,这块包括玩家游戏过程中的操作、游戏中的聊天等。其中,游戏开始前的信息占解压缩后的数据的前一小部分,紧接着后面的一大部分保存着游戏进行时的信息。

本文介绍如何解析游戏开始前的信息。

游戏开始前的信息的结构:

注:在下面各部分结构解释中,灰色字体标注的信息不对其进行解析,就不再详细介绍,要想了解可以参考w3g_format.txt文档。

一、总体结构

1、4 字节:未知。
2、variable字节:主机玩家记录(详细查看【二、玩家记录】)。
3、variable字节:游戏名称,字符串,以0x00结束。
4、1字节:空字节,0x00。
5、variable字节:特殊编码的数据(包括游戏设置、地图、创建者),以0x00结束(详细查看【三、特殊编码的数据】)。
6、4字节:玩家数量。
7、4字节:游戏类型。
8、4字节:未知。

9、variable字节:加入游戏的玩家列表(详细查看:【四、加入游戏的玩家列表】以及【二、玩家记录】)。
10、variable字节:Slot列表(详细查看:【五、Slot列表】)。

二、玩家记录

1、1字节:玩家类型,0x00主机,0x16加入游戏的玩家(【四、加入游戏的玩家列表】)。
2、1字节:玩家ID。
3、variable字节:玩家名称,以0x00结束。
4、1字节:附加数据大小,0x01或0x08。
5、1或8字节:附加数据。

三、特殊编码的数据

这是一段特殊编码的数据,该部分需要解码后才能继续解析,解码的方式直接看下面的代码,这里不再介绍。

解码后:
1、4字节:游戏设置,这部分包含一些高级选项,如下图,不过这部分很少有人去改变,所以这里不再去解析了。

Java解析魔兽争霸3录像W3G文件(三):解析游戏开始前的信息

2、5字节:未知。
3、4字节:地图校验。

4、variable字节:地图路径,字符串,以0x00结束。
5、variable字节:创建者,字符串,以0x00结束。

四、加入游戏的玩家列表

如果有多个玩家加入游戏,每个玩家对应一个下面的结构。由于是加入游戏的玩家,所以每个玩家对应的数据都是0x16开头。当遍历到第一个字节不是0x16时玩家列表就结束了。注意,加入游戏的玩家列表中不包含电脑玩家,电脑玩家在【五、Slot列表】中。

1、variable字节:玩家记录(详细查看【二、玩家记录】)。
2、4字节:0x00000000。

五、Slot列表

一个Slot是指游戏开始前的界面的一个玩家位置。如下图,即是4个Slot。

Java解析魔兽争霸3录像W3G文件(三):解析游戏开始前的信息

1、1字节:固定0x19。
2、2字节:下面的数据的字节数。

3、1字节:Slot数量。
4、variable字节:Slot记录的列表,其中包含多个Slot记录,数量即上面一个字节的值(详细查看【六、Slot记录】)。
5、4字节:随机种子。
6、1字节:队伍、种族是否可选择。
7、1字节:地图中的位置数量。

六、Slot记录

每个Slot占9个字节:

1、1字节:对应的玩家ID,电脑玩家是0x00。
2、1字节:地图下载百分比(一般都是100)。
3、1字节:Slot状态,0x00空的,0x01关闭着的,0x02使用中的。
4、1字节:是否是电脑玩家,0x00非电脑玩家,0x01电脑玩家。
5、1字节:队伍,0~11分别表示队伍1到队伍12,12表示裁判或观看者。
6、1字节:颜色,0红1蓝2青3紫4黄5橘黄6绿7粉8灰9浅蓝10深绿11棕12裁判或观看者
7、1字节:种族,0x01/0x41人族,0x02/0x42兽族,0x04/0x44暗夜精灵,0x08/0x48不死族,0x20/0x60随机。
8、1字节:电脑难度,0x00简单的,0x01中等难度的,0x02令人发狂的。
9、1字节:障碍(也就是血量百分比),0x32,0x3C,0x46,0x50,0x5A,0x64之一,分别表示50%到100%。

Java解析:

创建一个UncompressedData类,用于处理解压缩后的数据。

UncompressedData.java

package com.xxg.w3gparser;

import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

public class UncompressedData {

	/**
	 * 解压缩的字节数组
	 */
	private byte[] uncompressedDataBytes;
	
	/**
	 * 解析的字节位置
	 */
	private int offset;
	
	/**
	 * 玩家列表
	 */
	private List<Player> playerList = new ArrayList<Player>();
	
	/**
	 * 游戏名称
	 */
	private String gameName;
	
	/**
	 * 地图路径
	 */
	private String map;

	/**
	 * 游戏创建者名称
	 */
	private String createrName;
	
	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;
	}

	/**
	 *  解析PlayerRecode
	 *  @throws UnsupportedEncodingException
	 */
	private void analysisPlayerRecode() throws UnsupportedEncodingException {
		
		Player player = new Player();
		playerList.add(player);
		
		// 是否是主机(0为主机)
		byte isHostByte = uncompressedDataBytes[offset];
		boolean isHost = isHostByte == 0;
		player.setHost(isHost);
		offset++;
		
		// 玩家ID
		byte playerId = uncompressedDataBytes[offset];
		player.setPlayerId(playerId);
		offset++;
		
		// 玩家名称(UTF-8编码)
		int begin = offset;
		while(uncompressedDataBytes[offset] != 0) {
			offset++;
		}
		String playerName = new String(uncompressedDataBytes, begin, offset - begin, "UTF-8");
		player.setPlayerName(playerName);
		offset++;
		
		// 附加数据大小
		int additionalDataSize = uncompressedDataBytes[offset];
		offset++;
		
		// 加上附加数据大小
		offset += additionalDataSize;
		
	}
	
	/**
	 *  解析特殊编码的字节串
	 *  @throws UnsupportedEncodingException
	 */
	private void analysisEncodedBytes() throws UnsupportedEncodingException {
		
		int begin = offset;
		while(uncompressedDataBytes[offset] != 0) {
			offset++;
		}
		
		// 编码的数据和解码后的数据的长度
		int encodeLength = offset - begin - 1;
		int decodeLength = encodeLength - (encodeLength - 1) / 8 - 1;
		
		// 编码的数据和解码后的数据
		byte[] encodeData = new byte[encodeLength];
		byte[] decodeData = new byte[decodeLength];
		
		// 将编码字节串部分拷贝成一个单独的字节数组,便于解析
		System.arraycopy(uncompressedDataBytes, begin, encodeData, 0, encodeLength);
		
		// 解码(解码的代码来自于http://w3g.deepnode.de/files/w3g_format.txt文档4.3部分,由C语言代码翻译成Java)
		byte mask = 0;
		int decodePos = 0;
		int encodePos = 0;
		while (encodePos < encodeLength) {
			if (encodePos % 8 == 0) {
				mask = encodeData[encodePos];
			} else {
				if ((mask & (0x1 << (encodePos % 8))) == 0) {
					decodeData[decodePos++] = (byte) (encodeData[encodePos] - 1);
				} else {
					decodeData[decodePos++] = encodeData[encodePos];
				}
			}
			encodePos++;
		}
		
		// 直接跳过游戏设置,这部分不再解析了
		int decodeOffset = 13;
		int decodeBegin = decodeOffset;
		
		// 地图路径
		while(decodeData[decodeOffset] != 0) {
			decodeOffset++;
		}
		map = new String(decodeData, decodeBegin, decodeOffset - decodeBegin, "UTF-8");
		decodeOffset++;
		
		// 主机(游戏创建者)玩家名称
		decodeBegin = decodeOffset;
		while(decodeData[decodeOffset] != 0) {
			decodeOffset++;
		}
		createrName = new String(decodeData, decodeBegin, decodeOffset - decodeBegin, "UTF-8");
		decodeOffset++;
		
		offset++;
	}
	
	/**
	 *  解析每个Slot
	 */
	private void analysisSlotRecode(int slotNumber) {
		
		// 玩家ID
		byte playerId = uncompressedDataBytes[offset];
		offset++;
		
		// 跳过地图下载百分比
		offset++;
		
		// 状态 0空的 1关闭的 2使用的
		byte slotStatus = uncompressedDataBytes[offset];
		offset++;
		
		// 是否是电脑
		byte computerPlayFlag = uncompressedDataBytes[offset];
		boolean isComputer = computerPlayFlag == 1;
		offset++;
		
		// 队伍
		byte team = uncompressedDataBytes[offset];
		offset++;
		
		// 颜色
		byte color = uncompressedDataBytes[offset];
		offset++;
		
		// 种族
		byte race = uncompressedDataBytes[offset];
		offset++;
		
		// 电脑难度
		byte aiStrength = uncompressedDataBytes[offset];
		offset++;
		
		// 障碍(血量百分比)
		byte handicap = uncompressedDataBytes[offset];
		offset++;
		
		// 设置玩家列表
		if(slotStatus == 2) {
			Player player= null;
			if(!isComputer) {
				player = getPlayById(playerId);
			} else {
				player = new Player();
				playerList.add(player);
			}
			player.setComputer(isComputer);
			player.setAiStrength(aiStrength);
			player.setColor(color);
			player.setHandicap(handicap);
			player.setRace(race);
			player.setTeamNumber(team);
			player.setSlotNumber(slotNumber);
		}
		
	}
	
	/**
	 *  通过玩家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;
	}

	public List<Player> getPlayerList() {
		return playerList;
	}

	public String getGameName() {
		return gameName;
	}

	public String getMap() {
		return map;
	}

	public String getCreaterName() {
		return createrName;
	}

}

Player类表示每个玩家的信息,包括电脑玩家。其中slotNumber表示玩家的Slot位置,从0开始,后面将会用于解析聊天信息。

Player.java

package com.xxg.w3gparser;

public class Player {
	
	/**
	 * 是否是主机
	 */
	private boolean isHost;

	/**
	 * 玩家ID
	 */
	private byte playerId;
	
	/**
	 * 玩家的Slot位置
	 */
	private int slotNumber;

	/**
	 * 玩家名称
	 */
	private String playerName;
	
	/**
	 * 是否是电脑
	 */
	private boolean isComputer;
	
	/**
	 * 0~11:队伍1~12
	 * 12:裁判或观看者
	 */
	private byte teamNumber;
	
	/**
	 * 玩家颜色,0红1蓝2青3紫4黄5橘黄6绿7粉8灰9浅蓝10深绿11棕12裁判或观看者
	 */
	private byte color;
	
	/**
	 * 种族:0x01/0x41人族,0x02/0x42兽族,0x04/0x44暗夜精灵,0x08/0x48不死族,0x20/0x60随机
	 */
	private byte race;
	
	/**
	 * 电脑级别:0简单的,1中等难度的,2令人发狂的
	 */
	private byte aiStrength;
	
	/**
	 * 障碍,也就血量百分比,取值有50,60,70,80,90,100
	 */
	private byte handicap;

	public boolean isHost() {
		return isHost;
	}

	public void setHost(boolean isHost) {
		this.isHost = isHost;
	}

	public byte getPlayerId() {
		return playerId;
	}

	public void setPlayerId(byte playerId) {
		this.playerId = playerId;
	}

	public String getPlayerName() {
		return playerName;
	}

	public void setPlayerName(String playerName) {
		this.playerName = playerName;
	}

	public boolean isComputer() {
		return isComputer;
	}

	public void setComputer(boolean isComputer) {
		this.isComputer = isComputer;
	}

	public byte getTeamNumber() {
		return teamNumber;
	}

	public void setTeamNumber(byte teamNumber) {
		this.teamNumber = teamNumber;
	}

	public byte getColor() {
		return color;
	}

	public void setColor(byte color) {
		this.color = color;
	}

	public byte getRace() {
		return race;
	}

	public void setRace(byte race) {
		this.race = race;
	}

	public byte getAiStrength() {
		return aiStrength;
	}

	public void setAiStrength(byte aiStrength) {
		this.aiStrength = aiStrength;
	}

	public byte getHandicap() {
		return handicap;
	}

	public void setHandicap(byte handicap) {
		this.handicap = handicap;
	}

	public int getSlotNumber() {
		return slotNumber;
	}

	public void setSlotNumber(int slotNumber) {
		this.slotNumber = slotNumber;
	}

}

在Replay.java中,加入UncompressedData解析。

Replay.java

package com.xxg.w3gparser;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.zip.DataFormatException;

public class Replay {
	
	private Header header;
	
	private UncompressedData uncompressedData;
		
	public Replay(File w3gFile) throws IOException, W3GException, DataFormatException {
		
		// 将文件转为字节数组,方便处理
		byte[] fileBytes = fileToByteArray(w3gFile);
		
		// 解析Header
		header = new Header(fileBytes);

		// 遍历解析每个压缩数据块,解压缩,合并
		long compressedDataBlockCount = header.getCompressedDataBlockCount();
		byte[] uncompressedDataBytes = new byte[0]; // 所有压缩数据块中数据解压合并到这个数组中
		int offset = 68;
		for(int i = 0; i < compressedDataBlockCount; i++) {
			CompressedDataBlock compressedDataBlock = new CompressedDataBlock(fileBytes, offset);
			
			// 数组合并
			byte[] blockUncompressedData = compressedDataBlock.getUncompressedDataBytes();
			byte[] temp = new byte[uncompressedDataBytes.length + blockUncompressedData.length];
			System.arraycopy(uncompressedDataBytes, 0, temp, 0, uncompressedDataBytes.length);
			System.arraycopy(blockUncompressedData, 0, temp, uncompressedDataBytes.length, blockUncompressedData.length);
			uncompressedDataBytes = temp;
			
			int blockCompressedDataSize = compressedDataBlock.getCompressedDataSize();
			offset += 8 + blockCompressedDataSize;
		}
		
		// 处理解压缩后的字节数组
		uncompressedData = new UncompressedData(uncompressedDataBytes);
		
	}

	/**
	 * 将文件转换成字节数组
	 * @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;
	}

	public UncompressedData getUncompressedData() {
		return uncompressedData;
	}

}

修改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/桌面/131230_[UD]962030958_VS_[ORC]flygogogo_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());
        	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("玩家队伍:裁判或观看者");
        	}
        	
        }
        
	}
	
	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
时长:15:39
游戏名称:当地局域网内的游戏 (96
游戏创建者:962030958
游戏地图:Maps\E-WCLMAP\(2)AncientIsles.w3x
---玩家1---
玩家名称:962030958
是否主机:主机
SlotNumber:0
玩家队伍:1
玩家种族:不死族
玩家颜色:黄
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:flygogogo
是否主机:否
SlotNumber:1
玩家队伍:2
玩家种族:兽族
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否



参考文档:http://w3g.deepnode.de/files/w3g_format.txt




作者:叉叉哥   转载请注明出处:http://blog.csdn.net/xiao__gui/article/details/18218003




Java解析魔兽争霸3录像W3G文件(三):解析游戏开始前的信息

上一篇:生产者和消费者处理能力匹配的问题


下一篇:gcc源代码分析,rtx_alloc函数分析