在游戏进行中,玩家会进行各种操作,例如编队、移动、技能、造建筑等,这些操作就是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解析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;// 解析ActionanalysisAction(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解析魔兽争霸3录像W3G文件(五):Action和APM计算相关推荐

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

    上一篇博文中,通过对压缩数据块的解压缩以及合并,得到了解压缩的字节数组.从现在开始,就要处理这个数据. 这个部分的数据主要包括两大类信息:一类是游戏开始前的信息,例如游戏地图,游戏玩家,队伍.种族情况 ...

  2. java 解析m3u8的实例_m3u8文件完整实例及TS流抓取

    参考文档: http://blog.csdn.net/blueboyhi/article/details/40107683 如下 是一个华数影视的M3U8文件链接 http://chyd-sn.was ...

  3. 用Java解析:您可以使用的所有工具和库

    如果需要从Java解析语言或文档,则从根本上讲有三种方法可以解决问题: 使用支持该特定语言的现有库:例如用于解析XML的库 手动构建自己的自定义解析器 生成解析器的工具或库:例如ANTLR,可用于构建 ...

  4. java读取war3模型_GitHub - wucao/jw3gparser: Java Warcraft Ⅲ Replay Parser(Java解析《魔兽争霸3》游戏录像工具)...

    jw3gparser Java解析<魔兽争霸3>游戏录像工具,可解析w3g.nwg(网易对战平台录像)格式录像. 使用方法 public class Test { public stati ...

  5. java解析dxf文件_浅析JVM方法解析、创建和链接

    一:前言 上周末写了一篇文章<你知道Java类是如何被加载的吗?>,分析了HotSpot是如何加载Java类的,干脆趁热打铁,本周末再来分析下Hotspot又是如何解析.创建和链接类方法的 ...

  6. java解析Excel文件

    下文介绍java解析Excel文件的方案 前置准备 1.第三方jar包或者Maven配置 org.apache.poi的jar包 Maven配置如下 <groupId>org.apache ...

  7. XML解析 (JAVA解析xml文件)java+Dom4j+Xpath xml文件解析根据子节点得到父节点 查找校验xml文件中相同的节点属性值 java遍历文件夹解析XML

    XML解析 (JAVA解析xml文件)java+Dom4j+Xpath xml文件解析根据子节点得到父节点 以及查找xml文件中相同的节点属性值 项目背景:这是本人实习中所碰到的项目,当时感觉很棘手, ...

  8. IDEA Java解析GeoJson.json文件

    IDEA Java解析GeoJson.json文件 一.遇到的问题 1. 无法导入成功 2. org.geotools.StyleFactory is not an ImageIO SPI class ...

  9. java 解析 csv 文件

    文章分类:JavaEye 一.貌似有bug,不行用 二.或 三. 的方法 Java代码   import java.io.BufferedReader; import java.io.FileInpu ...

最新文章

  1. Python 进程之间共享数据(全局变量)
  2. linux系统的交换分区怎么分配?
  3. 阿里云助力重庆打造“亚洲最智能大型城市”
  4. 怎么把丢失的计算机放回桌面,不小心把电脑桌面开始哪里放在右边了,怎么把它放回原处啊...
  5. Java中的异步等待
  6. 简单的高可用集群实验
  7. java synchronized概念用法
  8. MATLAB画柱状图对比
  9. 大数据掀人类文明革命 探索更多未知
  10. 启动BPM的5个步骤
  11. 网站加载图片慢 网页响应慢 网页优化
  12. mmdetection报错 TypeError: vars() argument must have __dict__ attribute
  13. (附源码)ssm介绍信智能实现系统 毕业设计 260930
  14. php设计模式:单例模式
  15. 姑苏寻古[小刚执笔]
  16. VL817以及迭代型号VL817S原理图规格书示例
  17. safari开发模式联调h5,网页检查器空白
  18. 1-Click PC Tuneup软件-破解实录-[下]
  19. 2019中兴校招面经整理
  20. mysql查询面试_mysql查询面试一

热门文章

  1. 记录OpenJDK更新导致数据库SSL连接失败
  2. php curl 优化下载微信头像
  3. Windows电脑桌面云便签敬业签日历月视图怎么新增提醒事项?
  4. 同义词 synonym
  5. 电脑不支持MOV怎么办 怎么快速将mov格式转换为MP4 1
  6. 10.2.青蛙跳台问题
  7. Attention可视化
  8. zipslack安装方法(安装在ext3上成功运行)(转)
  9. vue element-upload上传视频或音频获取视频时长
  10. vue 项目打包通过命令修改 vue-router 模式,修改 API 接口前缀