Java解析魔兽争霸3录像W3G文件(四):解析游戏进行时的信息

上一篇博文中,通过解析压缩数据块解压缩后的数据的前一部分,可以获取到游戏开始前的一些信息,紧接着游戏开始前的信息之后,就是游戏进行时的信息了,其中包括玩家游戏中的操作,例如造建筑,出兵,攻击,移动等,还包括玩家游戏中的聊天信息,玩家退出游戏等。

游戏进行时的信息由很多个数据块组成。这些数据块有几种类型,每个数据块的第一个字节就是数据块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;

                // 未知的BlockId
                case 0x1A:
				case 0x1B:
				case 0x1C: 
					offset += 5;
					break;
                case 0x22:
                	offset += 6;
                	break;
                case 0x23:
                	offset += 11;
                	break;
                case 0x2F:
                	offset += 9;
                	break;

                // 无效的Block
                default:
                    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、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;
	
	// 游戏进行时的信息解析
	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文件(四):解析游戏进行时的信息

上一篇:遗传算法解决单变量函数值得优化问题


下一篇:Ubuntu上离线安装包的制作与安装