Java解析魔兽争霸3录像W3G文件(五):Action和APM计算

在游戏进行中,玩家会进行各种操作,例如编队、移动、技能、造建筑等,这些操作就是Action。APM(Actions Per Minute),表示每分钟的操作次数,APM可以很好的反映玩家的手速和实力,当然也有高APM的菜鸟和低APM的高手。

在魔兽录像文件中,需要记录下玩家的操作,这些操作是记录在游戏时间段(TimeSlot)数据块中的,这在上一篇博文中有提到。

结构:

在TimeSlot中从第6字节开始到数据块结尾的部分,包含多个玩家数据块(CommandData Block):

1字节:玩家ID;
2~3字节:数据块剩余字节数n;
4~n+4字节:包含该玩家对应的多个操作数据块(ActionBlock)。

ActionBlock结构:

1字节:ActionID,表示操作类型,例如暂停游戏操作的ActionID是0x01;
剩余字节:Action参数,该部分结构需要根据ActionID来确定,有些Action没有这部分。

由于Action类型很多,每种ActionID对应的ActionBlock结构这里不一一列出,下面列出一小部分:

1.暂停游戏
ActionID:0x01
字节数:1
计算APM:否

2.继续游戏
ActionID:0x02
字节数:1
计算APM:否

3.编队
ActionID:0x17
字节数:4+n*8
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3~4字节:选择单位的数量n;
5~4+n*8:选择单位

4.选择编队
ActionID:0x18
字节数:3
计算APM:是
结构:
1字节:ActionID;
2字节:队伍编号(0~9);
3字节:未知0x03

其他Action请参考文档:http://w3g.deepnode.de/files/w3g_actions.txt

APM计算:

由于暴雪官方并没有提供APM的计算方式,所以APM计算的方式都是前辈牛人们总结出来的,不同的录像分析软件算出来的APM可能会有一些误差。

APM的值等于玩家的有效Action数量除以玩家游戏时间的分钟数。

一个ActionBlock一般表示玩家的一次操作,例如一次编队、暂停游戏。其中部分操作要算入APM中,例如编队,而有些操作不计算APM,例如暂停游戏。另外,还有的ActionBlock是自动生成的,也不算入APM。Action是否算入APM可以查看文档w3g_actions.txt。

其中比较特殊的有ActionID为0x16的Action。这个Action表示选择或取消选择。ActionBlock的第二个字节为0x01表示选择,0x02表示取消选择。一般来说这个Action是算入APM的,但是如果两个相邻的ActionID为0x16的ActionBlock,前一个为取消选择,后一个为选择,那么这两个ActionBlock只算一次有效的Action,因为前一个是自动生成的。

在游戏进行过程中,可能会有玩家暂停游戏的情况,也有玩家在游戏结束前提前退出游戏的情况。在计算APM的时候一定要去掉这部分的时间,这样算出来的APM才准确。

下面的截图就是RepKing录像分析软件没有考虑游戏暂停导致的问题,导致玩家游戏时间大于录像的时长,APM计算不准确。

Java解析魔兽争霸3录像W3G文件(五):Action和APM计算_第1张图片

Java解析Action和APM:

在Player.java中加入action表示玩家的有效操作数:

/**
* 操作次数
*/
private int action;

public int getAction() {
	return action;
}

public void setAction(int action){
	this.action = action;
}
在ReplayData.java中,加入对Action的解析,在ReplayData中加入isPause表示游戏是否暂停,在游戏暂停时游戏时间不能增加:

/**
* 是否暂停
*/
private boolean isPause;

/**
*  解析一个时间块
*/
private void analysisTimeSlot() {
	
	offset++;
	
	int bytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);
	offset += 2;
	
	// 游戏时间在非暂停状态下增加
	int timeIncrement = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);
	if(!isPause) {
		time += timeIncrement;
	}
	offset += 2;
	
	// 解析Action
	analysisAction(offset + bytes - 2);
}

/**
 * 解析TimeSlot中的Action
 * @param end TimeSlot的结束位置
 */
private void analysisAction(int timeSlotEnd) {
	
	while(offset != timeSlotEnd) {
		
		byte playerId = uncompressedDataBytes[offset];
		Player player = getPlayById(playerId);
		int action = player.getAction();
		offset++;
		int commandDataBlockbytes = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);
		offset += 2;
		int commandDataBlockEnd = offset + commandDataBlockbytes;
		boolean lastActionWasDeselect = false;
		while(offset != commandDataBlockEnd) {
			
			byte actionId = uncompressedDataBytes[offset];
			
			boolean thisActionIsDeselect = false;
			if(actionId == 0x16 && uncompressedDataBytes[offset + 1] == 0x02) {
				thisActionIsDeselect = true;
			}
			
			switch (actionId) {
			
				// 暂停游戏
				case 0x01:
					isPause = true;
					offset++;
					break;
				
				// 继续游戏
				case 0x02:
					isPause = false;
					offset++;
					break;
				
				case 0x03:
					offset += 2;
					break;
				case 0x04:
				case 0x05:
					offset++;
					break;
				case 0x06:
					offset++;
					while(uncompressedDataBytes[offset] != 0) {
						offset++;
					}
					offset++;
					break;
				case 0x07:
					offset += 5;
					break;
				case 0x10:
					offset += 15;
					action++;
					break;
				case 0x11:
					offset += 23;
					action++;
					break;
				case 0x12:
					offset += 31;
					action++;
					break;
				case 0x13:
					offset += 39;
					action++;
					break;
				case 0x14:
					offset += 44;
					action++;
					break;
				case 0x16:
					offset++;
					byte selectMode = uncompressedDataBytes[offset];
					offset++;
					if(selectMode == 0x02) {
						action++;
					} else {
						if(!lastActionWasDeselect) {
							action++;
						}
					}
					int number = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);
					offset += 2;
					offset += number * 8;
					break;
				case 0X17:
					offset += 2;
					int n = LittleEndianTool.getUnsignedInt16(uncompressedDataBytes, offset);
					offset += 2;
					offset += n * 8;
					action++;
					break;
				case 0x18:
					offset += 3;
					action++;
					break;
				case 0x19:
					offset += 13;
					break;
				case 0x1a:
					offset++;
					break;
				case 0x1b:
					offset += 10;
					break;
				case 0x1c:
					offset += 10;
					action++;
					break;
				case 0x1d:
					offset += 9;
					action++;
					break;
				case 0x1e:
					offset += 6;
					action++;
					break;
				case 0x21:
					offset += 9;
					break;
				case 0x20:
				case 0x22:
				case 0x23:
				case 0x24:
				case 0x25:
				case 0x26:
				case 0x29:
				case 0x2a:
				case 0x2b:
				case 0x2c:
				case 0x2f:
				case 0x30:
				case 0x31:
				case 0x32:
					offset++;
					break;
				case 0x27:
				case 0x28:
				case 0x2d:
					offset += 6;
					break;
				case 0x2e:
					offset += 5;
					break;
				case 0x50:
					offset += 6;
					break;
				case 0x51:
					offset += 10;
					break;
				case 0x60:
					offset += 9;
					while(uncompressedDataBytes[offset] != 0) {
						offset++;
					}
					offset++;
					break;
				case 0x61:
					offset++;
					action++;
					break;
				case 0x62:
					offset += 13;
					break;
				case 0x66:
				case 0x67:
					offset++;
					action++;
					break;
				case 0x68:
					offset += 13;
					break;
				case 0x69:
				case 0x6a:
					offset += 17;
					break;
				case 0x75:
					offset += 2;
					break;
			}
			
			lastActionWasDeselect = thisActionIsDeselect;
		}
		
		player.setAction(action);
	}
}
在Test.java中,输出计算得到的玩家APM值:

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/桌面/131224_[UD]crabby_VS_[ORC]LuciferLVZ_LostTemple_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("是否主机:否");
        	}
        	System.out.println("游戏时间:" + convertMillisecondToString(player.getPlayTime()));
        	System.out.println("操作次数:" + player.getAction());
        	System.out.println("APM:" + player.getAction() * 60000 / player.getPlayTime());
        	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
时长:15:39
游戏名称:当地局域网内的游戏 (96
游戏创建者:962030958
游戏地图:Maps\E-WCLMAP\(2)AncientIsles.w3x
---玩家1---
玩家名称:962030958
是否主机:主机
游戏时间:15:38
操作次数:2635
APM:168
玩家队伍:1
玩家种族:不死族
玩家颜色:黄
障碍(血量):100%
是否电脑玩家:否
---玩家2---
玩家名称:flygogogo
是否主机:否
游戏时间:15:37
操作次数:3483
APM:222
玩家队伍:2
玩家种族:兽族
玩家颜色:蓝
障碍(血量):100%
是否电脑玩家:否
[0:12]962030958 对 所有人 说:glgl
[4:53]962030958 对 所有人 说:==
[4:53]962030958 对 所有人 说:猫咪爬到键盘上了
[4:53]962030958 对 所有人 说:g?
[4:53]flygogogo 对 所有人 说:g
[15:32]flygogogo 对 所有人 说:
[15:35]flygogogo 对 所有人 说:gg
[15:36]flygogogo 对 所有人 说:

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

结束语:

《Java解析魔兽争霸3录像W3G文件》系列博文就写到这里了,当然还有很多可以继续写的东西,例如判断玩家胜负,判断玩家的英雄、单位等。需要源码的同学可以在评论中留下E-mail。


Github地址:https://github.com/wucao/jw3gparser   欢迎star!


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


你可能感兴趣的:(java,录像,war3,魔兽争霸3,w3g)